Skip to content

Commit

Permalink
Add task backup import/export
Browse files Browse the repository at this point in the history
  • Loading branch information
pierotofy committed May 31, 2024
1 parent 86d9f38 commit 1add2fe
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 79 deletions.
20 changes: 20 additions & 0 deletions app/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,26 @@ def get(self, request, pk=None, project_pk=None, unsafe_asset_path=""):

return download_file_response(request, asset_path, 'inline')

"""
Task backup endpoint
"""
class TaskBackup(TaskNestedView):
def get(self, request, pk=None, project_pk=None):
"""
Downloads a task's backup
"""
task = self.get_and_check_task(request, pk)

# Check and download
try:
asset_fs = task.get_task_backup_stream()
except FileNotFoundError:
raise exceptions.NotFound(_("Asset does not exist"))

download_filename = request.GET.get('filename', get_asset_download_filename(task, "backup.zip"))

return download_file_stream(request, asset_fs, 'attachment', download_filename=download_filename)

"""
Task assets import
"""
Expand Down
3 changes: 2 additions & 1 deletion app/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from app.api.presets import PresetViewSet
from app.plugins.views import api_view_handler
from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskBackup, TaskAssetsImport
from .imageuploads import Thumbnail, ImageDownload
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
from .admin import AdminUserViewSet, AdminGroupViewSet, AdminProfileViewSet
Expand Down Expand Up @@ -46,6 +46,7 @@
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/import$', TaskAssetsImport.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/backup$', TaskBackup.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/thumbnail/(?P<image_filename>.+)$', Thumbnail.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/download/(?P<image_filename>.+)$', ImageDownload.as_view()),

Expand Down
66 changes: 63 additions & 3 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shutil
import time
import struct
from datetime import datetime
import uuid as uuid_module
from app.vendor import zipfly

Expand Down Expand Up @@ -453,6 +454,48 @@ def duplicate(self, set_new_name=True):
logger.warning("Cannot duplicate task: {}".format(str(e)))

return False

def write_backup_file(self):
"""Dump this tasks's fields to a backup file"""
with open(self.data_path("backup.json"), "w") as f:
f.write(json.dumps({
'name': self.name,
'processing_time': self.processing_time,
'options': self.options,
'created_at': self.created_at.timestamp(),
'public': self.public,
'resize_to': self.resize_to,
'potree_scene': self.potree_scene,
'tags': self.tags
}))

def read_backup_file(self):
"""Set this tasks fields based on the backup file (but don't save)"""
backup_file = self.data_path("backup.json")
if os.path.isfile(backup_file):
try:
with open(backup_file, "r") as f:
backup = json.loads(f.read())

self.name = backup.get('name', self.name)
self.processing_time = backup.get('processing_time', self.processing_time)
self.options = backup.get('options', self.options)
self.created_at = datetime.fromtimestamp(backup.get('created_at', self.created_at.timestamp()))
self.public = backup.get('public', self.public)
self.resize_to = backup.get('resize_to', self.resize_to)
self.potree_scene = backup.get('potree_scene', self.potree_scene)
self.tags = backup.get('tags', self.tags)

except Exception as e:
logger.warning("Cannot read backup file: %s" % str(e))

def get_task_backup_stream(self):
self.write_backup_file()
zip_dir = self.task_path("")
paths = [{'n': os.path.relpath(os.path.join(dp, f), zip_dir), 'fs': os.path.join(dp, f)} for dp, dn, filenames in os.walk(zip_dir) for f in filenames]
if len(paths) == 0:
raise FileNotFoundError("No files available for export")
return zipfly.ZipStream(paths)

def get_asset_file_or_stream(self, asset):
"""
Expand Down Expand Up @@ -568,7 +611,6 @@ def handle_import(self):
pass

self.pending_action = None
self.processing_time = 0
self.save()

def process(self):
Expand Down Expand Up @@ -859,10 +901,24 @@ def extract_assets_and_complete(self):
zip_h.extractall(assets_dir)

logger.info("Extracted all.zip for {}".format(self))

# Remove zip

os.remove(zip_path)

# Check if this looks like a backup file, in which case we need to move the files
# a directory level higher
is_backup = os.path.isfile(self.assets_path("data", "backup.json")) and os.path.isdir(self.assets_path("assets"))
if is_backup:
logger.info("Restoring from backup")
try:
tmp_dir = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, f"{self.id}.backup")

shutil.move(assets_dir, tmp_dir)
shutil.rmtree(self.task_path(""))
shutil.move(tmp_dir, self.task_path(""))
except shutil.Error as e:
logger.warning("Cannot restore from backup: %s" % str(e))
raise NodeServerError("Cannot restore from backup")

# Populate *_extent fields
extent_fields = [
(os.path.realpath(self.assets_path("odm_orthophoto", "odm_orthophoto.tif")),
Expand Down Expand Up @@ -906,6 +962,10 @@ def extract_assets_and_complete(self):
self.running_progress = 1.0
self.console += gettext("Done!") + "\n"
self.status = status_codes.COMPLETED

if is_backup:
self.read_backup_file()

self.save()

from app.plugins import signals as plugin_signals
Expand Down
3 changes: 3 additions & 0 deletions app/static/app/js/components/AssetDownloadButtons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class AssetDownloadButtons extends React.Component {
</li>);
}
})}
<li>
<a href={`/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/backup`}><i className="fa fa-file-download fa-fw"></i> {_("Backup")}</a>
</li>
</ul>
</div>);
}
Expand Down
4 changes: 2 additions & 2 deletions app/static/app/js/components/ImportTaskPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ class ImportTaskPanel extends React.Component {
<ErrorMessage bind={[this, 'error']} />

<button type="button" className="close theme-color-primary" title="Close" onClick={this.cancel}><span aria-hidden="true">&times;</span></button>
<h4>{_("Import Existing Assets")}</h4>
<p><Trans params={{arrow: '<i class="glyphicon glyphicon-arrow-right"></i>'}}>{_("You can import .zip files that have been exported from existing tasks via Download Assets %(arrow)s All Assets.")}</Trans></p>
<h4>{_("Import Assets or Backups")}</h4>
<p><Trans params={{arrow: '<i class="glyphicon glyphicon-arrow-right"></i>'}}>{_("You can import .zip files that have been exported from existing tasks via Download Assets %(arrow)s All Assets | Backup.")}</Trans></p>

<button disabled={this.state.uploading}
type="button"
Expand Down
Loading

0 comments on commit 1add2fe

Please sign in to comment.