Skip to content

Commit b5f9616

Browse files
author
Gauvain Pocentek
committed
Add support for project import/export
Fixes #471
1 parent 42007ec commit b5f9616

File tree

4 files changed

+169
-1
lines changed

4 files changed

+169
-1
lines changed

docs/gl_objects/projects.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,63 @@ Get a list of users for the repository::
180180
# search for users
181181
users = p.users.list(search='pattern')
182182

183+
Import / Export
184+
===============
185+
186+
You can export projects from gitlab, and re-import them to create new projects
187+
or overwrite existing ones.
188+
189+
Reference
190+
---------
191+
192+
* v4 API:
193+
194+
+ :class:`gitlab.v4.objects.ProjectExport`
195+
+ :class:`gitlab.v4.objects.ProjectExportManager`
196+
+ :attr:`gitlab.v4.objects.Project.exports`
197+
+ :class:`gitlab.v4.objects.ProjectImport`
198+
+ :class:`gitlab.v4.objects.ProjectImportManager`
199+
+ :attr:`gitlab.v4.objects.Project.imports`
200+
+ :attr:`gitlab.v4.objects.ProjectManager.import_project`
201+
202+
* GitLab API: https://docs.gitlab.com/ce/api/project_import_export.html
203+
204+
Examples
205+
--------
206+
207+
A project export is an asynchronous operation. To retrieve the archive
208+
generated by GitLab you need to:
209+
210+
#. Create an export using the API
211+
#. Wait for the export to be done
212+
#. Download the result
213+
214+
::
215+
216+
# Create the export
217+
p = gl.projects.get(my_project)
218+
export = p.exports.create({})
219+
220+
# Wait for the 'finished' status
221+
export.refresh()
222+
while export.export_status != 'finished':
223+
time.sleep(1)
224+
export.refresh()
225+
226+
# Download the result
227+
with open('/tmp/export.tgz', 'wb') as f:
228+
export.download(streamed=True, action=f.write)
229+
230+
Import the project::
231+
232+
gl.projects.import_project(open('/tmp/export.tgz', 'rb'), 'my_new_project')
233+
# Get a ProjectImport object to track the import status
234+
project_import = gl.projects.get(output['id'], lazy=True).imports.get()
235+
while project_import.import_status != 'finished':
236+
time.sleep(1)
237+
project_import.refresh()
238+
239+
183240
Project custom attributes
184241
=========================
185242

gitlab/mixins.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ def refresh(self, **kwargs):
8383
GitlabAuthenticationError: If authentication is not correct
8484
GitlabGetError: If the server cannot perform the request
8585
"""
86-
path = '%s/%s' % (self.manager.path, self.id)
86+
if self._id_attr:
87+
path = '%s/%s' % (self.manager.path, self.id)
88+
else:
89+
path = self.manager.path
8790
server_data = self.manager.gitlab.http_get(path, **kwargs)
8891
self._update_attrs(server_data)
8992

gitlab/v4/objects.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,6 +2400,53 @@ class ProjectWikiManager(CRUDMixin, RESTManager):
24002400
_list_filters = ('with_content', )
24012401

24022402

2403+
class ProjectExport(RefreshMixin, RESTObject):
2404+
_id_attr = None
2405+
2406+
@cli.register_custom_action('ProjectExport')
2407+
@exc.on_http_error(exc.GitlabGetError)
2408+
def download(self, streamed=False, action=None, chunk_size=1024, **kwargs):
2409+
"""Download the archive of a project export.
2410+
2411+
Args:
2412+
streamed (bool): If True the data will be processed by chunks of
2413+
`chunk_size` and each chunk is passed to `action` for
2414+
reatment
2415+
action (callable): Callable responsible of dealing with chunk of
2416+
data
2417+
chunk_size (int): Size of each chunk
2418+
**kwargs: Extra options to send to the server (e.g. sudo)
2419+
2420+
Raises:
2421+
GitlabAuthenticationError: If authentication is not correct
2422+
GitlabGetError: If the server failed to perform the request
2423+
2424+
Returns:
2425+
str: The blob content if streamed is False, None otherwise
2426+
"""
2427+
path = '/projects/%d/export/download' % self.project_id
2428+
result = self.manager.gitlab.http_get(path, streamed=streamed,
2429+
**kwargs)
2430+
return utils.response_content(result, streamed, action, chunk_size)
2431+
2432+
2433+
class ProjectExportManager(GetWithoutIdMixin, CreateMixin, RESTManager):
2434+
_path = '/projects/%(project_id)s/export'
2435+
_obj_cls = ProjectExport
2436+
_from_parent_attrs = {'project_id': 'id'}
2437+
_create_attrs = (tuple(), ('description',))
2438+
2439+
2440+
class ProjectImport(RefreshMixin, RESTObject):
2441+
_id_attr = None
2442+
2443+
2444+
class ProjectImportManager(GetWithoutIdMixin, RESTManager):
2445+
_path = '/projects/%(project_id)s/import'
2446+
_obj_cls = ProjectImport
2447+
_from_parent_attrs = {'project_id': 'id'}
2448+
2449+
24032450
class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
24042451
_short_print_attr = 'path'
24052452
_managers = (
@@ -2412,10 +2459,12 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
24122459
('deployments', 'ProjectDeploymentManager'),
24132460
('environments', 'ProjectEnvironmentManager'),
24142461
('events', 'ProjectEventManager'),
2462+
('exports', 'ProjectExportManager'),
24152463
('files', 'ProjectFileManager'),
24162464
('forks', 'ProjectForkManager'),
24172465
('hooks', 'ProjectHookManager'),
24182466
('keys', 'ProjectKeyManager'),
2467+
('imports', 'ProjectImportManager'),
24192468
('issues', 'ProjectIssueManager'),
24202469
('labels', 'ProjectLabelManager'),
24212470
('members', 'ProjectMemberManager'),
@@ -2847,6 +2896,41 @@ class ProjectManager(CRUDMixin, RESTManager):
28472896
'with_issues_enabled', 'with_merge_requests_enabled',
28482897
'custom_attributes')
28492898

2899+
def import_project(self, file, path, namespace=None, overwrite=False,
2900+
override_params=None, **kwargs):
2901+
"""Import a project from an archive file.
2902+
2903+
Args:
2904+
file: Data or file object containing the project
2905+
path (str): Name and path for the new project
2906+
namespace (str): The ID or path of the namespace that the project
2907+
will be imported to
2908+
overwrite (bool): If True overwrite an existing project with the
2909+
same path
2910+
override_params (dict): Set the specific settings for the project
2911+
**kwargs: Extra options to send to the server (e.g. sudo)
2912+
2913+
Raises:
2914+
GitlabAuthenticationError: If authentication is not correct
2915+
GitlabListError: If the server failed to perform the request
2916+
2917+
Returns:
2918+
dict: A representation of the import status.
2919+
"""
2920+
files = {
2921+
'file': ('file.tar.gz', file)
2922+
}
2923+
data = {
2924+
'path': path,
2925+
'overwrite': overwrite
2926+
}
2927+
if override_params:
2928+
data['override_params'] = override_params
2929+
if namespace:
2930+
data['namespace'] = namespace
2931+
return self.gitlab.http_post('/projects/import', post_data=data,
2932+
files=files, **kwargs)
2933+
28502934

28512935
class Runner(SaveMixin, ObjectDeleteMixin, RESTObject):
28522936
pass

tools/python_test_v4.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,27 @@
678678
[current_project.delete() for current_project in projects]
679679
settings.throttle_authenticated_api_enabled = False
680680
settings.save()
681+
682+
# project import/export
683+
ex = admin_project.exports.create({})
684+
ex.refresh()
685+
count = 0
686+
while ex.export_status != 'finished':
687+
time.sleep(1)
688+
ex.refresh()
689+
count += 1
690+
if count == 10:
691+
raise Exception('Project export taking too much time')
692+
with open('/tmp/gitlab-export.tgz', 'wb') as f:
693+
ex.download(streamed=True, action=f.write)
694+
695+
output = gl.projects.import_project(open('/tmp/gitlab-export.tgz', 'rb'),
696+
'imported_project')
697+
project_import = gl.projects.get(output['id'], lazy=True).imports.get()
698+
count = 0
699+
while project_import.import_status != 'finished':
700+
time.sleep(1)
701+
project_import.refresh()
702+
count += 1
703+
if count == 10:
704+
raise Exception('Project import taking too much time')

0 commit comments

Comments
 (0)