Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
setuptools
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jérome Perrin
setuptools
Commits
55b356b1
Commit
55b356b1
authored
Oct 02, 2009
by
Tarek Ziadé
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
#6516 added owner/group support for tarfiles in Distutils
parent
4c666278
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
218 additions
and
15 deletions
+218
-15
archive_util.py
archive_util.py
+66
-6
cmd.py
cmd.py
+5
-4
command/bdist.py
command/bdist.py
+13
-0
command/bdist_dumb.py
command/bdist_dumb.py
+11
-2
command/sdist.py
command/sdist.py
+8
-1
tests/test_archive_util.py
tests/test_archive_util.py
+61
-2
tests/test_sdist.py
tests/test_sdist.py
+54
-0
No files found.
archive_util.py
View file @
55b356b1
...
@@ -14,15 +14,55 @@ from distutils.spawn import spawn
...
@@ -14,15 +14,55 @@ from distutils.spawn import spawn
from
distutils.dir_util
import
mkpath
from
distutils.dir_util
import
mkpath
from
distutils
import
log
from
distutils
import
log
def
make_tarball
(
base_name
,
base_dir
,
compress
=
"gzip"
,
verbose
=
0
,
dry_run
=
0
):
try
:
from
pwd
import
getpwnam
except
AttributeError
:
getpwnam
=
None
try
:
from
grp
import
getgrnam
except
AttributeError
:
getgrnam
=
None
def
_get_gid
(
name
):
"""Returns a gid, given a group name."""
if
getgrnam
is
None
or
name
is
None
:
return
None
try
:
result
=
getgrnam
(
name
)
except
KeyError
:
result
=
None
if
result
is
not
None
:
return
result
[
2
]
return
None
def
_get_uid
(
name
):
"""Returns an uid, given a user name."""
if
getpwnam
is
None
or
name
is
None
:
return
None
try
:
result
=
getpwnam
(
name
)
except
KeyError
:
result
=
None
if
result
is
not
None
:
return
result
[
2
]
return
None
def
make_tarball
(
base_name
,
base_dir
,
compress
=
"gzip"
,
verbose
=
0
,
dry_run
=
0
,
owner
=
None
,
group
=
None
):
"""Create a (possibly compressed) tar file from all the files under
"""Create a (possibly compressed) tar file from all the files under
'base_dir'.
'base_dir'.
'compress' must be "gzip" (the default), "compress", "bzip2", or None.
'compress' must be "gzip" (the default), "compress", "bzip2", or None.
Both "tar" and the compression utility named by 'compress' must be on
(compress will be deprecated in Python 3.2)
the default program search path, so this is probably Unix-specific.
'owner' and 'group' can be used to define an owner and a group for the
archive that is being built. If not provided, the current owner and group
will be used.
The output tar file will be named 'base_dir' + ".tar", possibly plus
The output tar file will be named 'base_dir' + ".tar", possibly plus
the appropriate compression extension (".gz", ".bz2" or ".Z").
the appropriate compression extension (".gz", ".bz2" or ".Z").
Returns the output filename.
Returns the output filename.
"""
"""
tar_compression
=
{
'gzip'
:
'gz'
,
'bzip2'
:
'bz2'
,
None
:
''
,
'compress'
:
''
}
tar_compression
=
{
'gzip'
:
'gz'
,
'bzip2'
:
'bz2'
,
None
:
''
,
'compress'
:
''
}
...
@@ -44,10 +84,23 @@ def make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0):
...
@@ -44,10 +84,23 @@ def make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0):
import
tarfile
# late import so Python build itself doesn't break
import
tarfile
# late import so Python build itself doesn't break
log
.
info
(
'Creating tar archive'
)
log
.
info
(
'Creating tar archive'
)
uid
=
_get_uid
(
owner
)
gid
=
_get_gid
(
group
)
def
_set_uid_gid
(
tarinfo
):
if
gid
is
not
None
:
tarinfo
.
gid
=
gid
tarinfo
.
gname
=
group
if
uid
is
not
None
:
tarinfo
.
uid
=
uid
tarinfo
.
uname
=
owner
return
tarinfo
if
not
dry_run
:
if
not
dry_run
:
tar
=
tarfile
.
open
(
archive_name
,
'w|%s'
%
tar_compression
[
compress
])
tar
=
tarfile
.
open
(
archive_name
,
'w|%s'
%
tar_compression
[
compress
])
try
:
try
:
tar
.
add
(
base_dir
)
tar
.
add
(
base_dir
,
filter
=
_set_uid_gid
)
finally
:
finally
:
tar
.
close
()
tar
.
close
()
...
@@ -138,7 +191,7 @@ def check_archive_formats(formats):
...
@@ -138,7 +191,7 @@ def check_archive_formats(formats):
return
None
return
None
def
make_archive
(
base_name
,
format
,
root_dir
=
None
,
base_dir
=
None
,
verbose
=
0
,
def
make_archive
(
base_name
,
format
,
root_dir
=
None
,
base_dir
=
None
,
verbose
=
0
,
dry_run
=
0
):
dry_run
=
0
,
owner
=
None
,
group
=
None
):
"""Create an archive file (eg. zip or tar).
"""Create an archive file (eg. zip or tar).
'base_name' is the name of the file to create, minus any format-specific
'base_name' is the name of the file to create, minus any format-specific
...
@@ -151,6 +204,9 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
...
@@ -151,6 +204,9 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
ie. 'base_dir' will be the common prefix of all files and
ie. 'base_dir' will be the common prefix of all files and
directories in the archive. 'root_dir' and 'base_dir' both default
directories in the archive. 'root_dir' and 'base_dir' both default
to the current directory. Returns the name of the archive file.
to the current directory. Returns the name of the archive file.
'owner' and 'group' are used when creating a tar archive. By default,
uses the current owner and group.
"""
"""
save_cwd
=
os
.
getcwd
()
save_cwd
=
os
.
getcwd
()
if
root_dir
is
not
None
:
if
root_dir
is
not
None
:
...
@@ -172,8 +228,12 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
...
@@ -172,8 +228,12 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
func
=
format_info
[
0
]
func
=
format_info
[
0
]
for
arg
,
val
in
format_info
[
1
]:
for
arg
,
val
in
format_info
[
1
]:
kwargs
[
arg
]
=
val
kwargs
[
arg
]
=
val
filename
=
apply
(
func
,
(
base_name
,
base_dir
),
kwargs
)
if
format
!=
'zip'
:
kwargs
[
'owner'
]
=
owner
kwargs
[
'group'
]
=
group
filename
=
apply
(
func
,
(
base_name
,
base_dir
),
kwargs
)
if
root_dir
is
not
None
:
if
root_dir
is
not
None
:
log
.
debug
(
"changing back to '%s'"
,
save_cwd
)
log
.
debug
(
"changing back to '%s'"
,
save_cwd
)
os
.
chdir
(
save_cwd
)
os
.
chdir
(
save_cwd
)
...
...
cmd.py
View file @
55b356b1
...
@@ -385,10 +385,11 @@ class Command:
...
@@ -385,10 +385,11 @@ class Command:
from
distutils.spawn
import
spawn
from
distutils.spawn
import
spawn
spawn
(
cmd
,
search_path
,
dry_run
=
self
.
dry_run
)
spawn
(
cmd
,
search_path
,
dry_run
=
self
.
dry_run
)
def
make_archive
(
self
,
base_name
,
format
,
def
make_archive
(
self
,
base_name
,
format
,
root_dir
=
None
,
base_dir
=
None
,
root_dir
=
None
,
base_dir
=
None
):
owner
=
None
,
group
=
None
):
return
archive_util
.
make_archive
(
return
archive_util
.
make_archive
(
base_name
,
format
,
root_dir
,
base_name
,
format
,
root_dir
,
base_dir
,
dry_run
=
self
.
dry_run
)
base_dir
,
dry_run
=
self
.
dry_run
,
owner
=
owner
,
group
=
group
)
def
make_file
(
self
,
infiles
,
outfile
,
func
,
args
,
def
make_file
(
self
,
infiles
,
outfile
,
func
,
args
,
exec_msg
=
None
,
skip_msg
=
None
,
level
=
1
):
exec_msg
=
None
,
skip_msg
=
None
,
level
=
1
):
...
...
command/bdist.py
View file @
55b356b1
...
@@ -40,6 +40,12 @@ class bdist(Command):
...
@@ -40,6 +40,12 @@ class bdist(Command):
"[default: dist]"
),
"[default: dist]"
),
(
'skip-build'
,
None
,
(
'skip-build'
,
None
,
"skip rebuilding everything (for testing/debugging)"
),
"skip rebuilding everything (for testing/debugging)"
),
(
'owner='
,
'u'
,
"Owner name used when creating a tar file"
" [default: current user]"
),
(
'group='
,
'g'
,
"Group name used when creating a tar file"
" [default: current group]"
),
]
]
boolean_options
=
[
'skip-build'
]
boolean_options
=
[
'skip-build'
]
...
@@ -81,6 +87,8 @@ class bdist(Command):
...
@@ -81,6 +87,8 @@ class bdist(Command):
self
.
formats
=
None
self
.
formats
=
None
self
.
dist_dir
=
None
self
.
dist_dir
=
None
self
.
skip_build
=
0
self
.
skip_build
=
0
self
.
group
=
None
self
.
owner
=
None
def
finalize_options
(
self
):
def
finalize_options
(
self
):
# have to finalize 'plat_name' before 'bdist_base'
# have to finalize 'plat_name' before 'bdist_base'
...
@@ -126,6 +134,11 @@ class bdist(Command):
...
@@ -126,6 +134,11 @@ class bdist(Command):
if
cmd_name
not
in
self
.
no_format_option
:
if
cmd_name
not
in
self
.
no_format_option
:
sub_cmd
.
format
=
self
.
formats
[
i
]
sub_cmd
.
format
=
self
.
formats
[
i
]
# passing the owner and group names for tar archiving
if
cmd_name
==
'bdist_dumb'
:
sub_cmd
.
owner
=
self
.
owner
sub_cmd
.
group
=
self
.
group
# If we're going to need to run this command again, tell it to
# If we're going to need to run this command again, tell it to
# keep its temporary files around so subsequent runs go faster.
# keep its temporary files around so subsequent runs go faster.
if
cmd_name
in
commands
[
i
+
1
:]:
if
cmd_name
in
commands
[
i
+
1
:]:
...
...
command/bdist_dumb.py
View file @
55b356b1
...
@@ -36,6 +36,12 @@ class bdist_dumb (Command):
...
@@ -36,6 +36,12 @@ class bdist_dumb (Command):
(
'relative'
,
None
,
(
'relative'
,
None
,
"build the archive using relative paths"
"build the archive using relative paths"
"(default: false)"
),
"(default: false)"
),
(
'owner='
,
'u'
,
"Owner name used when creating a tar file"
" [default: current user]"
),
(
'group='
,
'g'
,
"Group name used when creating a tar file"
" [default: current group]"
),
]
]
boolean_options
=
[
'keep-temp'
,
'skip-build'
,
'relative'
]
boolean_options
=
[
'keep-temp'
,
'skip-build'
,
'relative'
]
...
@@ -53,6 +59,8 @@ class bdist_dumb (Command):
...
@@ -53,6 +59,8 @@ class bdist_dumb (Command):
self
.
dist_dir
=
None
self
.
dist_dir
=
None
self
.
skip_build
=
0
self
.
skip_build
=
0
self
.
relative
=
0
self
.
relative
=
0
self
.
owner
=
None
self
.
group
=
None
def
finalize_options
(
self
):
def
finalize_options
(
self
):
if
self
.
bdist_dir
is
None
:
if
self
.
bdist_dir
is
None
:
...
@@ -71,7 +79,7 @@ class bdist_dumb (Command):
...
@@ -71,7 +79,7 @@ class bdist_dumb (Command):
(
'dist_dir'
,
'dist_dir'
),
(
'dist_dir'
,
'dist_dir'
),
(
'plat_name'
,
'plat_name'
))
(
'plat_name'
,
'plat_name'
))
def
run
(
self
):
def
run
(
self
):
if
not
self
.
skip_build
:
if
not
self
.
skip_build
:
self
.
run_command
(
'build'
)
self
.
run_command
(
'build'
)
...
@@ -110,7 +118,8 @@ class bdist_dumb (Command):
...
@@ -110,7 +118,8 @@ class bdist_dumb (Command):
# Make the archive
# Make the archive
filename
=
self
.
make_archive
(
pseudoinstall_root
,
filename
=
self
.
make_archive
(
pseudoinstall_root
,
self
.
format
,
root_dir
=
archive_root
)
self
.
format
,
root_dir
=
archive_root
,
owner
=
self
.
owner
,
group
=
self
.
group
)
if
self
.
distribution
.
has_ext_modules
():
if
self
.
distribution
.
has_ext_modules
():
pyversion
=
get_python_version
()
pyversion
=
get_python_version
()
else
:
else
:
...
...
command/sdist.py
View file @
55b356b1
...
@@ -74,6 +74,10 @@ class sdist(Command):
...
@@ -74,6 +74,10 @@ class sdist(Command):
(
'medata-check'
,
None
,
(
'medata-check'
,
None
,
"Ensure that all required elements of meta-data "
"Ensure that all required elements of meta-data "
"are supplied. Warn if any missing. [default]"
),
"are supplied. Warn if any missing. [default]"
),
(
'owner='
,
'u'
,
"Owner name used when creating a tar file [default: current user]"
),
(
'group='
,
'g'
,
"Group name used when creating a tar file [default: current group]"
),
]
]
boolean_options
=
[
'use-defaults'
,
'prune'
,
boolean_options
=
[
'use-defaults'
,
'prune'
,
...
@@ -113,6 +117,8 @@ class sdist(Command):
...
@@ -113,6 +117,8 @@ class sdist(Command):
self
.
archive_files
=
None
self
.
archive_files
=
None
self
.
metadata_check
=
1
self
.
metadata_check
=
1
self
.
owner
=
None
self
.
group
=
None
def
finalize_options
(
self
):
def
finalize_options
(
self
):
if
self
.
manifest
is
None
:
if
self
.
manifest
is
None
:
...
@@ -455,7 +461,8 @@ class sdist(Command):
...
@@ -455,7 +461,8 @@ class sdist(Command):
self.formats.append(self.formats.pop(self.formats.index('tar')))
self.formats.append(self.formats.pop(self.formats.index('tar')))
for fmt in self.formats:
for fmt in self.formats:
file = self.make_archive(base_name, fmt, base_dir=base_dir)
file = self.make_archive(base_name, fmt, base_dir=base_dir,
owner=self.owner, group=self.group)
archive_files.append(file)
archive_files.append(file)
self.distribution.dist_files.append(('sdist', '', file))
self.distribution.dist_files.append(('sdist', '', file))
...
...
tests/test_archive_util.py
View file @
55b356b1
...
@@ -13,6 +13,13 @@ from distutils.spawn import find_executable, spawn
...
@@ -13,6 +13,13 @@ from distutils.spawn import find_executable, spawn
from
distutils.tests
import
support
from
distutils.tests
import
support
from
test.test_support
import
check_warnings
from
test.test_support
import
check_warnings
try
:
import
grp
import
pwd
UID_GID_SUPPORT
=
True
except
ImportError
:
UID_GID_SUPPORT
=
False
try
:
try
:
import
zipfile
import
zipfile
ZIP_SUPPORT
=
True
ZIP_SUPPORT
=
True
...
@@ -30,7 +37,7 @@ class ArchiveUtilTestCase(support.TempdirManager,
...
@@ -30,7 +37,7 @@ class ArchiveUtilTestCase(support.TempdirManager,
support
.
LoggingSilencer
,
support
.
LoggingSilencer
,
unittest
.
TestCase
):
unittest
.
TestCase
):
@
unittest
.
skipUnless
(
zlib
,
"
R
equires zlib"
)
@
unittest
.
skipUnless
(
zlib
,
"
r
equires zlib"
)
def
test_make_tarball
(
self
):
def
test_make_tarball
(
self
):
# creating something to tar
# creating something to tar
tmpdir
=
self
.
mkdtemp
()
tmpdir
=
self
.
mkdtemp
()
...
@@ -41,7 +48,7 @@ class ArchiveUtilTestCase(support.TempdirManager,
...
@@ -41,7 +48,7 @@ class ArchiveUtilTestCase(support.TempdirManager,
tmpdir2
=
self
.
mkdtemp
()
tmpdir2
=
self
.
mkdtemp
()
unittest
.
skipUnless
(
splitdrive
(
tmpdir
)[
0
]
==
splitdrive
(
tmpdir2
)[
0
],
unittest
.
skipUnless
(
splitdrive
(
tmpdir
)[
0
]
==
splitdrive
(
tmpdir2
)[
0
],
"
S
ource and target should be on same drive"
)
"
s
ource and target should be on same drive"
)
base_name
=
os
.
path
.
join
(
tmpdir2
,
'archive'
)
base_name
=
os
.
path
.
join
(
tmpdir2
,
'archive'
)
...
@@ -202,6 +209,58 @@ class ArchiveUtilTestCase(support.TempdirManager,
...
@@ -202,6 +209,58 @@ class ArchiveUtilTestCase(support.TempdirManager,
base_name
=
os
.
path
.
join
(
tmpdir
,
'archive'
)
base_name
=
os
.
path
.
join
(
tmpdir
,
'archive'
)
self
.
assertRaises
(
ValueError
,
make_archive
,
base_name
,
'xxx'
)
self
.
assertRaises
(
ValueError
,
make_archive
,
base_name
,
'xxx'
)
def
test_make_archive_owner_group
(
self
):
# testing make_archive with owner and group, with various combinations
# this works even if there's not gid/uid support
if
UID_GID_SUPPORT
:
group
=
grp
.
getgrgid
(
0
)[
0
]
owner
=
pwd
.
getpwuid
(
0
)[
0
]
else
:
group
=
owner
=
'root'
base_dir
,
root_dir
,
base_name
=
self
.
_create_files
()
base_name
=
os
.
path
.
join
(
self
.
mkdtemp
()
,
'archive'
)
res
=
make_archive
(
base_name
,
'zip'
,
root_dir
,
base_dir
,
owner
=
owner
,
group
=
group
)
self
.
assertTrue
(
os
.
path
.
exists
(
res
))
res
=
make_archive
(
base_name
,
'zip'
,
root_dir
,
base_dir
)
self
.
assertTrue
(
os
.
path
.
exists
(
res
))
res
=
make_archive
(
base_name
,
'tar'
,
root_dir
,
base_dir
,
owner
=
owner
,
group
=
group
)
self
.
assertTrue
(
os
.
path
.
exists
(
res
))
res
=
make_archive
(
base_name
,
'tar'
,
root_dir
,
base_dir
,
owner
=
'kjhkjhkjg'
,
group
=
'oihohoh'
)
self
.
assertTrue
(
os
.
path
.
exists
(
res
))
@
unittest
.
skipUnless
(
zlib
,
"Requires zlib"
)
@
unittest
.
skipUnless
(
UID_GID_SUPPORT
,
"Requires grp and pwd support"
)
def
test_tarfile_root_owner
(
self
):
tmpdir
,
tmpdir2
,
base_name
=
self
.
_create_files
()
old_dir
=
os
.
getcwd
()
os
.
chdir
(
tmpdir
)
group
=
grp
.
getgrgid
(
0
)[
0
]
owner
=
pwd
.
getpwuid
(
0
)[
0
]
try
:
archive_name
=
make_tarball
(
base_name
,
'dist'
,
compress
=
None
,
owner
=
owner
,
group
=
group
)
finally
:
os
.
chdir
(
old_dir
)
# check if the compressed tarball was created
self
.
assertTrue
(
os
.
path
.
exists
(
archive_name
))
# now checks the rights
archive
=
tarfile
.
open
(
archive_name
)
try
:
for
member
in
archive
.
getmembers
():
self
.
assertEquals
(
member
.
uid
,
0
)
self
.
assertEquals
(
member
.
gid
,
0
)
finally
:
archive
.
close
()
def
test_suite
():
def
test_suite
():
return
unittest
.
makeSuite
(
ArchiveUtilTestCase
)
return
unittest
.
makeSuite
(
ArchiveUtilTestCase
)
...
...
tests/test_sdist.py
View file @
55b356b1
...
@@ -3,6 +3,7 @@ import os
...
@@ -3,6 +3,7 @@ import os
import
unittest
import
unittest
import
shutil
import
shutil
import
zipfile
import
zipfile
import
tarfile
# zlib is not used here, but if it's not available
# zlib is not used here, but if it's not available
# the tests that use zipfile may fail
# the tests that use zipfile may fail
...
@@ -11,6 +12,13 @@ try:
...
@@ -11,6 +12,13 @@ try:
except
ImportError
:
except
ImportError
:
zlib
=
None
zlib
=
None
try
:
import
grp
import
pwd
UID_GID_SUPPORT
=
True
except
ImportError
:
UID_GID_SUPPORT
=
False
from
os.path
import
join
from
os.path
import
join
import
sys
import
sys
import
tempfile
import
tempfile
...
@@ -288,6 +296,52 @@ class SDistTestCase(PyPIRCCommandTestCase):
...
@@ -288,6 +296,52 @@ class SDistTestCase(PyPIRCCommandTestCase):
cmd
.
formats
=
'supazipa'
cmd
.
formats
=
'supazipa'
self
.
assertRaises
(
DistutilsOptionError
,
cmd
.
finalize_options
)
self
.
assertRaises
(
DistutilsOptionError
,
cmd
.
finalize_options
)
@
unittest
.
skipUnless
(
zlib
,
"requires zlib"
)
@
unittest
.
skipUnless
(
UID_GID_SUPPORT
,
"Requires grp and pwd support"
)
def
test_make_distribution_owner_group
(
self
):
# check if tar and gzip are installed
if
(
find_executable
(
'tar'
)
is
None
or
find_executable
(
'gzip'
)
is
None
):
return
# now building a sdist
dist
,
cmd
=
self
.
get_cmd
()
# creating a gztar and specifying the owner+group
cmd
.
formats
=
[
'gztar'
]
cmd
.
owner
=
pwd
.
getpwuid
(
0
)[
0
]
cmd
.
group
=
grp
.
getgrgid
(
0
)[
0
]
cmd
.
ensure_finalized
()
cmd
.
run
()
# making sure we have the good rights
archive_name
=
join
(
self
.
tmp_dir
,
'dist'
,
'fake-1.0.tar.gz'
)
archive
=
tarfile
.
open
(
archive_name
)
try
:
for
member
in
archive
.
getmembers
():
self
.
assertEquals
(
member
.
uid
,
0
)
self
.
assertEquals
(
member
.
gid
,
0
)
finally
:
archive
.
close
()
# building a sdist again
dist
,
cmd
=
self
.
get_cmd
()
# creating a gztar
cmd
.
formats
=
[
'gztar'
]
cmd
.
ensure_finalized
()
cmd
.
run
()
# making sure we have the good rights
archive_name
=
join
(
self
.
tmp_dir
,
'dist'
,
'fake-1.0.tar.gz'
)
archive
=
tarfile
.
open
(
archive_name
)
try
:
for
member
in
archive
.
getmembers
():
self
.
assertEquals
(
member
.
uid
,
os
.
getuid
())
self
.
assertEquals
(
member
.
gid
,
os
.
getgid
())
finally
:
archive
.
close
()
def
test_suite
():
def
test_suite
():
return
unittest
.
makeSuite
(
SDistTestCase
)
return
unittest
.
makeSuite
(
SDistTestCase
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment