X Tutup
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/gl_objects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,29 @@
# board lists delete
b_list.delete()
# end board lists delete

# project file upload by path
# Or provide a full path to the uploaded file
project.upload("filename.txt", filepath="/some/path/filename.txt")
# end project file upload by path

# project file upload with data
# Upload a file using its filename and filedata
project.upload("filename.txt", filedata="Raw data")
# end project file upload with data

# project file upload markdown
uploaded_file = project.upload_file("filename.txt", filedata="data")
issue = project.issues.get(issue_id)
issue.notes.create({
"body": "See the attached file: {}".format(uploaded_file["markdown"])
})
# project file upload markdown

# project file upload markdown custom
uploaded_file = project.upload_file("filename.txt", filedata="data")
issue = project.issues.get(issue_id)
issue.notes.create({
"body": "See the [attached file]({})".format(uploaded_file["url"])
})
# project file upload markdown
48 changes: 48 additions & 0 deletions docs/gl_objects/projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,51 @@ Delete a list:
.. literalinclude:: projects.py
:start-after: # board lists delete
:end-before: # end board lists delete


File Uploads
============

Reference
---------

* v4 API:

+ :attr:`gitlab.v4.objects.Project.upload`
+ :class:`gitlab.v4.objects.ProjectUpload`

* v3 API:

+ :attr:`gitlab.v3.objects.Project.upload`
+ :class:`gitlab.v3.objects.ProjectUpload`

* Gitlab API: https://docs.gitlab.com/ce/api/projects.html#upload-a-file

Examples
--------

Upload a file into a project using a filesystem path:

.. literalinclude:: projects.py
:start-after: # project file upload by path
:end-before: # end project file upload by path

Upload a file into a project without a filesystem path:

.. literalinclude:: projects.py
:start-after: # project file upload with data
:end-before: # end project file upload with data

Upload a file and comment on an issue using the uploaded file's
markdown:

.. literalinclude:: projects.py
:start-after: # project file upload markdown
:end-before: # end project file upload markdown

Upload a file and comment on an issue while using custom
markdown to reference the uploaded file:

.. literalinclude:: projects.py
:start-after: # project file upload markdown custom
:end-before: # end project file upload markdown custom
20 changes: 14 additions & 6 deletions gitlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,11 +396,13 @@ def _raw_list(self, path_, cls, **kwargs):

return results

def _raw_post(self, path_, data=None, content_type=None, **kwargs):
def _raw_post(self, path_, data=None, content_type=None,
files=None, **kwargs):
url = '%s%s' % (self._url, path_)
opts = self._get_session_opts(content_type)
try:
return self.session.post(url, params=kwargs, data=data, **opts)
return self.session.post(url, params=kwargs, data=data,
files=files, **opts)
except Exception as e:
raise GitlabConnectionError(
"Can't connect to GitLab server (%s)" % e)
Expand Down Expand Up @@ -628,7 +630,7 @@ def _build_url(self, path):
return '%s%s' % (self._url, path)

def http_request(self, verb, path, query_data={}, post_data={},
streamed=False, **kwargs):
streamed=False, files=None, **kwargs):
"""Make an HTTP request to the Gitlab server.

Args:
Expand Down Expand Up @@ -658,6 +660,11 @@ def sanitized_url(url):
params = query_data.copy()
params.update(kwargs)
opts = self._get_session_opts(content_type='application/json')

# don't set the content-type header when uploading files
if files is not None:
del opts["headers"]["Content-type"]

verify = opts.pop('verify')
timeout = opts.pop('timeout')

Expand All @@ -668,7 +675,7 @@ def sanitized_url(url):
# always agree with this decision (this is the case with a default
# gitlab installation)
req = requests.Request(verb, url, json=post_data, params=params,
**opts)
files=files, **opts)
prepped = self.session.prepare_request(req)
prepped.url = sanitized_url(prepped.url)
result = self.session.send(prepped, stream=streamed, verify=verify,
Expand Down Expand Up @@ -754,7 +761,8 @@ def http_list(self, path, query_data={}, as_list=None, **kwargs):
# No pagination, generator requested
return GitlabList(self, url, query_data, **kwargs)

def http_post(self, path, query_data={}, post_data={}, **kwargs):
def http_post(self, path, query_data={}, post_data={}, files=None,
**kwargs):
"""Make a POST request to the Gitlab server.

Args:
Expand All @@ -774,7 +782,7 @@ def http_post(self, path, query_data={}, post_data={}, **kwargs):
GitlabParsingError: If the json data could not be parsed
"""
result = self.http_request('post', path, query_data=query_data,
post_data=post_data, **kwargs)
post_data=post_data, files=files, **kwargs)
try:
if result.headers.get('Content-Type', None) == 'application/json':
return result.json()
Expand Down
2 changes: 1 addition & 1 deletion gitlab/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def __ne__(self, other):
class RESTObject(object):
"""Represents an object built from server data.

It holds the attributes know from te server, and the updated attributes in
It holds the attributes know from the server, and the updated attributes in
another. This allows smart updates, if the object allows it.

You can redefine ``_id_attr`` in child classes to specify which attribute
Expand Down
8 changes: 8 additions & 0 deletions gitlab/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ class GitlabTimeTrackingError(GitlabOperationError):
pass


class GitlabUploadError(GitlabOperationError):
pass


class GitlabAttachFileError(GitlabOperationError):
pass


class GitlabCherryPickError(GitlabOperationError):
pass

Expand Down
21 changes: 20 additions & 1 deletion gitlab/v3/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
'unstar': {'required': ['id']},
'archive': {'required': ['id']},
'unarchive': {'required': ['id']},
'share': {'required': ['id', 'group-id', 'group-access']}},
'share': {'required': ['id', 'group-id', 'group-access']},
'upload': {'required': ['id', 'filename', 'filepath']}},
gitlab.v3.objects.User: {
'block': {'required': ['id']},
'unblock': {'required': ['id']},
Expand Down Expand Up @@ -348,6 +349,20 @@ def do_user_getbyusername(self, cls, gl, what, args):
except Exception as e:
cli.die("Impossible to get user %s" % args['query'], e)

def do_project_upload(self, cls, gl, what, args):
try:
project = gl.projects.get(args["id"])
except Exception as e:
cli.die("Could not load project '{!r}'".format(args["id"]), e)

try:
res = project.upload(filename=args["filename"],
filepath=args["filepath"])
except Exception as e:
cli.die("Could not upload file into project", e)

return res


def _populate_sub_parser_by_class(cls, sub_parser):
for action_name in ['list', 'get', 'create', 'update', 'delete']:
Expand Down Expand Up @@ -469,6 +484,7 @@ def run(gl, what, action, args, verbose, *fargs, **kwargs):
cli.die("Unknown object: %s" % what)

g_cli = GitlabCLI()

method = None
what = what.replace('-', '_')
action = action.lower().replace('-', '')
Expand All @@ -491,6 +507,9 @@ def run(gl, what, action, args, verbose, *fargs, **kwargs):
print("")
else:
print(o)
elif isinstance(ret_val, dict):
for k, v in six.iteritems(ret_val):
print("{} = {}".format(k, v))
elif isinstance(ret_val, gitlab.base.GitlabObject):
ret_val.display(verbose)
elif isinstance(ret_val, six.string_types):
Expand Down
63 changes: 63 additions & 0 deletions gitlab/v3/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@ class ProjectIssueNote(GitlabObject):
requiredCreateAttrs = ['body']
optionalCreateAttrs = ['created_at']

# file attachment settings (see #56)
description_attr = "body"
project_id_attr = "project_id"


class ProjectIssueNoteManager(BaseManager):
obj_cls = ProjectIssueNote
Expand All @@ -933,6 +937,10 @@ class ProjectIssue(GitlabObject):
[('project_id', 'project_id'), ('issue_id', 'id')]),
)

# file attachment settings (see #56)
description_attr = "description"
project_id_attr = "project_id"

def subscribe(self, **kwargs):
"""Subscribe to an issue.

Expand Down Expand Up @@ -1057,6 +1065,7 @@ class ProjectIssueManager(BaseManager):

class ProjectMember(GitlabObject):
_url = '/projects/%(project_id)s/members'

requiredUrlAttrs = ['project_id']
requiredCreateAttrs = ['access_level', 'user_id']
optionalCreateAttrs = ['expires_at']
Expand Down Expand Up @@ -2096,6 +2105,60 @@ def trigger_build(self, ref, token, variables={}, **kwargs):
r = self.gitlab._raw_post(url, data=data, **kwargs)
raise_error_from_response(r, GitlabCreateError, 201)

# see #56 - add file attachment features
def upload(self, filename, filedata=None, filepath=None, **kwargs):
"""Upload the specified file into the project.

.. note::

Either ``filedata`` or ``filepath`` *MUST* be specified.

Args:
filename (str): The name of the file being uploaded
filedata (bytes): The raw data of the file being uploaded
filepath (str): The path to a local file to upload (optional)

Raises:
GitlabConnectionError: If the server cannot be reached
GitlabUploadError: If the file upload fails
GitlabUploadError: If ``filedata`` and ``filepath`` are not
specified
GitlabUploadError: If both ``filedata`` and ``filepath`` are
specified

Returns:
dict: A ``dict`` with the keys:
* ``alt`` - The alternate text for the upload
* ``url`` - The direct url to the uploaded file
* ``markdown`` - Markdown for the uploaded file
"""
if filepath is None and filedata is None:
raise GitlabUploadError("No file contents or path specified")

if filedata is not None and filepath is not None:
raise GitlabUploadError("File contents and file path specified")

if filepath is not None:
with open(filepath, "rb") as f:
filedata = f.read()

url = ("/projects/%(id)s/uploads" % {
"id": self.id,
})
r = self.gitlab._raw_post(
url,
files={"file": (filename, filedata)},
)
# returns 201 status code (created)
raise_error_from_response(r, GitlabUploadError, expected_code=201)
data = r.json()

return {
"alt": data['alt'],
"url": data['url'],
"markdown": data['markdown']
}


class Runner(GitlabObject):
_url = '/runners'
Expand Down
2 changes: 2 additions & 0 deletions gitlab/v4/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ def get_dict(obj):
else:
print(obj)
print('')
elif isinstance(ret_val, dict):
printer.display(ret_val, verbose=verbose, obj=ret_val)
elif isinstance(ret_val, gitlab.base.RESTObject):
printer.display(get_dict(ret_val), verbose=verbose, obj=ret_val)
elif isinstance(ret_val, six.string_types):
Expand Down
53 changes: 53 additions & 0 deletions gitlab/v4/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,59 @@ def trigger_pipeline(self, ref, token, variables={}, **kwargs):
post_data.update(form)
self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)

# see #56 - add file attachment features
@cli.register_custom_action('Project', ('filename', 'filepath'))
@exc.on_http_error(exc.GitlabUploadError)
def upload(self, filename, filedata=None, filepath=None, **kwargs):
"""Upload the specified file into the project.

.. note::

Either ``filedata`` or ``filepath`` *MUST* be specified.

Args:
filename (str): The name of the file being uploaded
filedata (bytes): The raw data of the file being uploaded
filepath (str): The path to a local file to upload (optional)

Raises:
GitlabConnectionError: If the server cannot be reached
GitlabUploadError: If the file upload fails
GitlabUploadError: If ``filedata`` and ``filepath`` are not
specified
GitlabUploadError: If both ``filedata`` and ``filepath`` are
specified

Returns:
dict: A ``dict`` with the keys:
* ``alt`` - The alternate text for the upload
* ``url`` - The direct url to the uploaded file
* ``markdown`` - Markdown for the uploaded file
"""
if filepath is None and filedata is None:
raise GitlabUploadError("No file contents or path specified")

if filedata is not None and filepath is not None:
raise GitlabUploadError("File contents and file path specified")

if filepath is not None:
with open(filepath, "rb") as f:
filedata = f.read()

url = ('/projects/%(id)s/uploads' % {
'id': self.id,
})
file_info = {
'file': (filename, filedata),
}
data = self.manager.gitlab.http_post(url, files=file_info)

return {
"alt": data['alt'],
"url": data['url'],
"markdown": data['markdown']
}


class Runner(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
Expand Down
4 changes: 4 additions & 0 deletions tools/cli_test_v3.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ testcase "branch deletion" '
--name branch1 >/dev/null 2>&1
'

testcase "project upload" '
GITLAB project upload --id "$PROJECT_ID" --filename '$(basename $0)' --filepath '$0'
'

testcase "project deletion" '
GITLAB project delete --id "$PROJECT_ID"
'
Loading
X Tutup