From e02ccd6fb022e56bf35a9bb3e59d351be21782eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98JoinTyang=E2=80=99?= Date: Thu, 8 Aug 2024 15:02:54 +0800 Subject: [PATCH] import and export sdoc --- .../src/components/cur-dir-path/dir-path.js | 2 + frontend/src/components/cur-dir-path/index.js | 2 + .../components/dialog/upload-sdoc-dialog.js | 82 +++++++++++ .../dirent-grid-view/dirent-grid-view.js | 23 ++- .../dirent-list-view/dirent-list-item.js | 11 ++ .../toolbar/dir-operation-toolbar.js | 23 ++- .../lib-content-view/lib-content-container.js | 1 + frontend/src/utils/text-translation.js | 1 + frontend/src/utils/utils.js | 3 +- seahub/api2/authentication.py | 18 ++- seahub/api2/urls.py | 1 + seahub/api2/views.py | 137 +++++++++++++++++- seahub/seadoc/apis.py | 56 ++++++- seahub/seadoc/urls.py | 3 +- seahub/seadoc/utils.py | 101 ++++++++++++- seahub/views/file.py | 7 + 16 files changed, 455 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/dialog/upload-sdoc-dialog.js diff --git a/frontend/src/components/cur-dir-path/dir-path.js b/frontend/src/components/cur-dir-path/dir-path.js index 3e90425c46f..ca10c882ad3 100644 --- a/frontend/src/components/cur-dir-path/dir-path.js +++ b/frontend/src/components/cur-dir-path/dir-path.js @@ -34,6 +34,7 @@ const propTypes = { filePermission: PropTypes.string, onFileTagChanged: PropTypes.func.isRequired, onItemMove: PropTypes.func.isRequired, + loadDirentList: PropTypes.func.isRequired, }; class DirPath extends React.Component { @@ -177,6 +178,7 @@ class DirPath extends React.Component { onAddFolder={this.props.onAddFolder} onUploadFile={this.props.onUploadFile} onUploadFolder={this.props.onUploadFolder} + loadDirentList={this.props.loadDirentList} > {item} diff --git a/frontend/src/components/cur-dir-path/index.js b/frontend/src/components/cur-dir-path/index.js index 4f59e3787f2..2f51a059317 100644 --- a/frontend/src/components/cur-dir-path/index.js +++ b/frontend/src/components/cur-dir-path/index.js @@ -40,6 +40,7 @@ const propTypes = { onFileTagChanged: PropTypes.func.isRequired, metadataViewId: PropTypes.string, onItemMove: PropTypes.func.isRequired, + loadDirentList: PropTypes.func.isRequired, }; class CurDirPath extends React.Component { @@ -86,6 +87,7 @@ class CurDirPath extends React.Component { onFileTagChanged={this.props.onFileTagChanged} repoTags={this.props.repoTags} onItemMove={this.props.onItemMove} + loadDirentList={this.props.loadDirentList} /> {isDesktop && { + this.props.toggle(); + }; + + openFileInput = () => { + this.fileInputRef.current.click(); + }; + + uploadSdoc = (file) => { + toaster.notify(gettext('It may take some time, please wait.')); + let { repoID, itemPath } = this.props; + seafileAPI.importSdoc(file, repoID, itemPath).then((res) => { + this.props.loadDirentList(itemPath); + }).catch((error) => { + let errMsg = Utils.getErrorMsg(error); + toaster.danger(errMsg); + }); + }; + + uploadFile = (e) => { + // no file selected + if (!this.fileInputRef.current.files.length) { + return; + } + // check file extension + let fileName = this.fileInputRef.current.files[0].name; + if (fileName.substr(fileName.lastIndexOf('.') + 1) != 'zsdoc') { + this.setState({ + errorMsg: gettext('Please choose a .zsdoc file.') + }); + return; + } + const file = this.fileInputRef.current.files[0]; + this.uploadSdoc(file); + this.toggle(); + }; + + render() { + let { errorMsg } = this.state; + return ( + + {gettext('Import sdoc')} + + + + {errorMsg && {errorMsg}} + + + + + + ); + } +} + +UploadSdocDialog.propTypes = propTypes; + +export default UploadSdocDialog; diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index a255e19e592..b3d6582bdb1 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -295,10 +295,24 @@ class DirentGridView extends React.Component { exportDocx = () => { const serviceUrl = window.app.config.serviceURL; + let dirent = this.state.activeDirent ? this.state.activeDirent : ''; + if (!dirent) { + return; + } let repoID = this.props.repoID; - let filePath = this.getDirentPath(this.props.dirent); - let exportToDocxUrl = serviceUrl + '/repo/sdoc_export_to_docx/' + repoID + '/?file_path=' + filePath; - window.location.href = exportToDocxUrl; + let filePath = this.getDirentPath(dirent); + window.location.href = serviceUrl + '/repo/sdoc_export_to_docx/' + repoID + '/?file_path=' + filePath; + }; + + exportSdoc = () => { + const serviceUrl = window.app.config.serviceURL; + let dirent = this.state.activeDirent ? this.state.activeDirent : ''; + if (!dirent) { + return; + } + let repoID = this.props.repoID; + let filePath = this.getDirentPath(dirent); + window.location.href = serviceUrl + '/lib/' + repoID + '/file/' + filePath + '?dl=1'; }; onMenuItemClick = (operation, currentObject, event) => { @@ -337,6 +351,9 @@ class DirentGridView extends React.Component { case 'Export docx': this.exportDocx(); break; + case 'Export sdoc': + this.exportSdoc(); + break; case 'Convert to sdoc': this.onItemConvert(currentObject, event, 'sdoc'); break; diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 80fc26efbf7..a5a463e4476 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -237,6 +237,14 @@ class DirentListItem extends React.Component { window.location.href = exportToDocxUrl; }; + exportSdoc = () => { + const serviceUrl = window.app.config.serviceURL; + let repoID = this.props.repoID; + let filePath = this.getDirentPath(this.props.dirent); + let exportToSdocUrl = serviceUrl + '/lib/' + repoID + '/file/' + filePath + '?dl=1'; + window.location.href = exportToSdocUrl; + }; + closeSharedDialog = () => { this.setState({ isShareDialogShow: !this.state.isShareDialogShow }); }; @@ -293,6 +301,9 @@ class DirentListItem extends React.Component { case 'Export docx': this.exportDocx(); break; + case 'Export sdoc': + this.exportSdoc(); + break; case 'Convert to sdoc': this.onItemConvert(event, 'sdoc'); break; diff --git a/frontend/src/components/toolbar/dir-operation-toolbar.js b/frontend/src/components/toolbar/dir-operation-toolbar.js index 00fa7cff8fa..b834212bf3c 100644 --- a/frontend/src/components/toolbar/dir-operation-toolbar.js +++ b/frontend/src/components/toolbar/dir-operation-toolbar.js @@ -7,6 +7,7 @@ import ModalPortal from '../modal-portal'; import CreateFolder from '../../components/dialog/create-folder-dialog'; import CreateFile from '../../components/dialog/create-file-dialog'; import ShareDialog from '../../components/dialog/share-dialog'; +import UploadSdocDialog from '../dialog/upload-sdoc-dialog'; const propTypes = { path: PropTypes.string.isRequired, @@ -22,7 +23,8 @@ const propTypes = { onUploadFile: PropTypes.func.isRequired, onUploadFolder: PropTypes.func.isRequired, direntList: PropTypes.array.isRequired, - children: PropTypes.object + children: PropTypes.object, + loadDirentList: PropTypes.func }; class DirOperationToolbar extends React.Component { @@ -37,7 +39,8 @@ class DirOperationToolbar extends React.Component { operationMenuStyle: '', isDesktopMenuOpen: false, isSubMenuShown: false, - isMobileOpMenuOpen: false + isMobileOpMenuOpen: false, + isUploadSdocDialogOpen: false, }; } @@ -156,6 +159,10 @@ class DirOperationToolbar extends React.Component { } }; + onToggleUploadSdoc = () => { + this.setState({ isUploadSdocDialogOpen: !this.state.isUploadSdocDialogOpen }); + }; + render() { let { path, repoName, userPerm } = this.props; @@ -185,6 +192,10 @@ class DirOperationToolbar extends React.Component { 'icon': 'upload-files', 'text': gettext('Upload Folder'), 'onClick': this.onUploadFolder + }, { + 'icon': 'upload-sdoc', + 'text': gettext('Upload Sdoc'), + 'onClick': this.onToggleUploadSdoc }); } else { opList.push({ @@ -348,6 +359,14 @@ class DirOperationToolbar extends React.Component { /> } + {this.state.isUploadSdocDialogOpen && + + } ); } diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index 76b0ba03402..1b1b5d3f6e3 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -224,6 +224,7 @@ class LibContentContainer extends React.Component { repoTags={this.props.repoTags} metadataViewId={this.props.metadataViewId} onItemMove={this.props.onItemMove} + loadDirentList={this.props.loadDirentList} /> [-0-9a-f]{36})/upload-shared-links/$', RepoUploadSharedLinks.as_view(), name="api2-repo-upload-shared-links"), re_path(r'^repos/(?P[-0-9a-f]{36})/upload-shared-links/(?P[a-f0-9]+)/$', RepoUploadSharedLink.as_view(), name="api2-repo-upload-shared-link"), re_path(r'^repos/(?P[-0-9a-f]{36})/upload-link/$', UploadLinkView.as_view()), + re_path(r'^repos/(?P[-0-9a-f]{36})/import-sdoc/$', ImportSdoc.as_view()), re_path(r'^repos/(?P[-0-9a-f]{36})/update-link/$', UpdateLinkView.as_view()), re_path(r'^repos/(?P[-0-9a-f]{36})/upload-blks-link/$', UploadBlksLinkView.as_view()), re_path(r'^repos/(?P[-0-9a-f]{36})/update-blks-link/$', UpdateBlksLinkView.as_view()), diff --git a/seahub/api2/views.py b/seahub/api2/views.py index c566349723a..e1ce9e70c45 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -8,8 +8,12 @@ import datetime import posixpath import re +import uuid from dateutil.relativedelta import relativedelta from urllib.parse import quote +import requests +import shutil +from zipfile import is_zipfile, ZipFile from rest_framework import parsers from rest_framework import status @@ -29,9 +33,10 @@ from django.template.defaultfilters import filesizeformat from django.utils import timezone from django.utils.translation import gettext as _ +from django.core.files.uploadhandler import TemporaryFileUploadHandler from .throttling import ScopedRateThrottle, AnonRateThrottle, UserRateThrottle -from .authentication import TokenAuthentication +from .authentication import TokenAuthentication, CsrfExemptSessionAuthentication from .serializers import AuthTokenSerializer from .utils import get_diff_details, to_python_boolean, \ api_error, get_file_size, prepare_starred_files, is_web_request, \ @@ -109,6 +114,7 @@ ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, SHARE_LINK_EXPIRE_DAYS_MAX, \ SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_DEFAULT from seahub.subscription.utils import subscription_check +from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_image_parent_path, get_seadoc_asset_upload_link try: from seahub.settings import CLOUD_MODE @@ -1999,6 +2005,135 @@ def get(self, request, repo_id, format=None): return Response(url) + +class ImportSdoc(APIView): + authentication_classes = (TokenAuthentication, CsrfExemptSessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def post(self, request, repo_id): + # use TemporaryFileUploadHandler, which contains TemporaryUploadedFile + # TemporaryUploadedFile has temporary_file_path() method + # in order to change upload_handlers, we must exempt csrf check + request.upload_handlers = [TemporaryFileUploadHandler(request=request)] + username = request.user.username + relative_path = request.data.get('relative_path', '/').strip('/') + parent_dir = request.data.get('parent_dir', '/') + replace = request.data.get('replace', 'False') + try: + replace = to_python_boolean(replace) + except ValueError: + error_msg = 'replace invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + file = request.FILES.get('file', None) + if not file: + error_msg = 'file can not be found.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + filename = file.name + uploaded_temp_path = file.temporary_file_path() + extension = filename.split('.')[-1].lower() + + if not (extension == 'zsdoc' and is_zipfile(uploaded_temp_path)): + error_msg = 'file format not supported.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_dir) + if not dir_id: + error_msg = 'Folder %s not found.' % parent_dir + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_upload is False: + return api_error(status.HTTP_403_FORBIDDEN, 'You do not have permission to access this folder.') + + if check_quota(repo_id) < 0: + return api_error(HTTP_443_ABOVE_QUOTA, _("Out of quota.")) + + obj_id = json.dumps({'parent_dir': parent_dir}) + try: + token = seafile_api.get_fileserver_access_token(repo_id, obj_id, 'upload', username, use_onetime=False) + except Exception as e: + if str(e) == 'Too many files in library.': + error_msg = _("The number of files in library exceeds the limit") + return api_error(HTTP_447_TOO_MANY_FILES_IN_LIBRARY, error_msg) + else: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not token: + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + upload_link = gen_file_upload_url(token, 'upload-api') + upload_link += '?ret-json=1' + if replace: + upload_link += '&replace=1' + + # upload file + tmp_dir = str(uuid.uuid4()) + tmp_extracted_path = os.path.join('/tmp/seahub', str(repo_id), 'sdoc_zip_extracted/', tmp_dir) + try: + with ZipFile(uploaded_temp_path) as zip_file: + zip_file.extractall(tmp_extracted_path) + except Exception as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + sdoc_file_name = filename.replace('zsdoc', 'sdoc') + new_file_path = os.path.join(parent_dir, relative_path, sdoc_file_name) + + data = {'parent_dir': parent_dir, 'target_file': new_file_path, 'relative_path': relative_path} + if replace: + data['replace'] = 1 + sdoc_file_path = os.path.join(tmp_extracted_path, 'content.json') + new_sdoc_file_path = os.path.join(tmp_extracted_path, sdoc_file_name) + os.rename(sdoc_file_path, new_sdoc_file_path) + + files = {'file': open(new_sdoc_file_path, 'rb')} + resp = requests.post(upload_link, files=files, data=data) + if not resp.ok: + logger.error('save file: %s failed: %s' % (filename, resp.text)) + return api_error(resp.status_code, resp.content) + + sdoc_name = json.loads(resp.content)[0].get('name') + new_sdoc_file_path = os.path.join(parent_dir, relative_path, sdoc_name) + doc_uuid = get_seadoc_file_uuid(repo, new_sdoc_file_path) + + # upload sdoc images + image_dir = os.path.join(tmp_extracted_path, 'images/') + batch_upload_sdoc_images(doc_uuid, repo_id, username, image_dir) + + # remove tmp file + if os.path.exists(tmp_extracted_path): + shutil.rmtree(tmp_extracted_path) + + return Response({'success': True}) + + +def batch_upload_sdoc_images(doc_uuid, repo_id, username, image_dir): + parent_path = gen_seadoc_image_parent_path(doc_uuid, repo_id, username) + upload_link = get_seadoc_asset_upload_link(repo_id, parent_path, username) + + file_list = os.listdir(image_dir) + + for filename in file_list: + file_path = posixpath.join(parent_path, filename) + image_path = os.path.join(image_dir, filename) + image_file = open(image_path, 'rb') + files = {'file': image_file} + data = {'parent_dir': parent_path, 'filename': filename, 'target_file': file_path} + resp = requests.post(upload_link, files=files, data=data) + if not resp.ok: + logger.warning('upload sdoc image: %s failed: %s', filename, resp.text) + + class UpdateLinkView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index 994dbdd8b68..8e8051a6a2f 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -6,8 +6,9 @@ import logging import requests import posixpath -from urllib.parse import unquote +from urllib.parse import unquote, quote import time +import shutil from datetime import datetime, timedelta from pypinyin import lazy_pinyin @@ -17,7 +18,7 @@ from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated from django.utils.translation import gettext as _ -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponseRedirect, HttpResponse, FileResponse from django.core.files.base import ContentFile from django.utils import timezone from django.db import transaction @@ -32,7 +33,7 @@ from seahub.seadoc.utils import is_valid_seadoc_access_token, get_seadoc_upload_link, \ get_seadoc_download_link, get_seadoc_file_uuid, gen_seadoc_access_token, \ gen_seadoc_image_parent_path, get_seadoc_asset_upload_link, get_seadoc_asset_download_link, \ - can_access_seadoc_asset, is_seadoc_revision + can_access_seadoc_asset, is_seadoc_revision, ZSDOC, export_sdoc from seahub.seadoc.settings import SDOC_REVISIONS_DIR, SDOC_IMAGES_DIR from seahub.utils.file_types import SEADOC, IMAGE from seahub.utils.file_op import if_locked_by_online_office @@ -799,7 +800,7 @@ def get(self, request, file_uuid): logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - + name_dict = {} obj_id_list = [commit.file_id for commit in file_revisions] if obj_id_list: @@ -1362,7 +1363,7 @@ def post(self, request, file_uuid, comment_id): detail = { 'author': username, 'comment_id': int(comment_id), - 'reply_id': reply.pk, + 'reply_id': reply.pk, 'reply' : str(reply_content), 'msg_type': 'reply', 'created_at': datetime_to_isoformat_timestr(reply.created_at), @@ -3020,6 +3021,49 @@ def get(self, request, file_uuid): else: f['repo_id'] = real_repo_id f['fullpath'] = f['fullpath'].split(origin_path)[-1] - f['doc_uuid'] = get_seadoc_file_uuid(repo, e['fullpath']) + f['doc_uuid'] = get_seadoc_file_uuid(repo, f['fullpath']) return Response(resp_json, resp.status_code) + + +class SeadocExportView(APIView): + authentication_classes = (SdocJWTTokenAuthentication, TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, file_uuid): + username = request.user.username + uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid) + if not uuid_map: + error_msg = 'seadoc uuid %s not found.' % file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + filetype, fileext = get_file_type_and_ext(uuid_map.filename) + if filetype != SEADOC: + error_msg = 'seadoc file type %s invalid.' % filetype + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # permission check + permission = check_folder_permission(request, uuid_map.repo_id, uuid_map.parent_path) + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + tmp_zip_path = export_sdoc(uuid_map, username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + if not os.path.exists(tmp_zip_path): + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + response = FileResponse(open(tmp_zip_path, 'rb'), content_type="application/x-zip-compressed", as_attachment=True) + response['Content-Disposition'] = 'attachment;filename*=UTF-8\'\'' + quote(uuid_map.filename[:-4] + ZSDOC) + + tmp_dir = os.path.join('/tmp/sdoc', str(uuid_map.uuid)) + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) + + return response diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index 7f63c29931c..bfcbf889c9e 100644 --- a/seahub/seadoc/urls.py +++ b/seahub/seadoc/urls.py @@ -5,7 +5,7 @@ SeadocCommentRepliesView, SeadocCommentReplyView, SeadocFileView, SeadocFileUUIDView, SeadocDirView, SdocRevisionBaseVersionContent, SeadocRevisionView, \ SdocRepoTagsView, SdocRepoTagView, SdocRepoFileTagsView, SdocRepoFileTagView, SeadocNotificationsView, SeadocNotificationView, \ SeadocFilesInfoView, DeleteSeadocOtherRevision, SeadocPublishedRevisionContent, SdocParticipantsView, SdocParticipantView, SdocRelatedUsers, SeadocEditorCallBack, \ - SeadocDailyHistoryDetail, SeadocSearchFilenameView + SeadocDailyHistoryDetail, SeadocSearchFilenameView, SeadocExportView # api/v2.1/seadoc/ urlpatterns = [ @@ -51,4 +51,5 @@ re_path(r'^notifications/(?P[-0-9a-f]{36})/$', SeadocNotificationsView.as_view(), name='seadoc_notifications'), re_path(r'^notifications/(?P[-0-9a-f]{36})/(?P\d+)/$', SeadocNotificationView.as_view(), name='seadoc_notification'), re_path(r'^search-filename/(?P[-0-9a-f]{36})/$', SeadocSearchFilenameView.as_view(), name='seadoc_search_filename'), + re_path(r'^export/(?P[-0-9a-f]{36})/$', SeadocExportView.as_view(), name='seadoc_export'), ] diff --git a/seahub/seadoc/utils.py b/seahub/seadoc/utils.py index 289cc12203e..039d36abc12 100644 --- a/seahub/seadoc/utils.py +++ b/seahub/seadoc/utils.py @@ -1,16 +1,21 @@ import os +import io import jwt import json import time import uuid import logging import posixpath +import shutil +import requests +from zipfile import ZipFile, is_zipfile from seaserv import seafile_api from seahub.tags.models import FileUUIDMap from seahub.settings import SEADOC_PRIVATE_KEY -from seahub.utils import normalize_file_path, gen_file_get_url, gen_file_upload_url, gen_inner_file_get_url +from seahub.utils import normalize_file_path, gen_file_get_url, gen_file_upload_url, gen_inner_file_get_url, \ + get_inner_fileserver_root from seahub.utils.auth import AUTHORIZATION_PREFIX from seahub.views import check_folder_permission from seahub.base.templatetags.seahub_tags import email2nickname @@ -20,6 +25,8 @@ logger = logging.getLogger(__name__) +ZSDOC = 'zsdoc' + def uuid_str_to_32_chars(file_uuid): if len(file_uuid) == 36: @@ -320,3 +327,95 @@ def move_sdoc_images(src_repo_id, src_path, dst_repo_id, dst_path, username, is_ need_progress=need_progress, synchronous=synchronous, ) return + + +def export_sdoc_clear_tmp_files_and_dirs(tmp_file_path, tmp_zip_path): + # delete tmp files/dirs + if os.path.exists(tmp_file_path): + shutil.rmtree(tmp_file_path) + if os.path.exists(tmp_zip_path): + os.remove(tmp_zip_path) + + +def export_sdoc_prepare_images_folder(repo_id, doc_uuid, images_dir_id, username): + # get file server access token + fake_obj_id = { + 'obj_id': images_dir_id, + 'dir_name': 'images', # after download and zip, folder root name is images + 'is_windows': 0 + } + try: + token = seafile_api.get_fileserver_access_token( + repo_id, json.dumps(fake_obj_id), 'download-dir', username, use_onetime=False + ) + except Exception as e: + raise e + + progress = {'zipped': 0, 'total': 1} + while progress['zipped'] != progress['total']: + time.sleep(0.5) # sleep 0.5 second + try: + progress = json.loads(seafile_api.query_zip_progress(token)) + except Exception as e: + raise e + + asset_url = '%s/zip/%s' % (get_inner_fileserver_root(), token) + try: + resp = requests.get(asset_url) + except Exception as e: + raise e + file_obj = io.BytesIO(resp.content) + if is_zipfile(file_obj): + with ZipFile(file_obj) as zp: + zp.extractall(os.path.join('/tmp/sdoc', doc_uuid, 'sdoc_asset')) + return + + +def export_sdoc(uuid_map, username): + """ + /tmp/sdoc//sdoc_asset/ + |- images/ + |- content.json + zip /tmp/sdoc//sdoc_asset/ to /tmp/sdoc//zip_file.zip + """ + doc_uuid = str(uuid_map.uuid) + repo_id = uuid_map.repo_id + + logger.info('Start prepare /tmp/sdoc/{}/zip_file.zip for export sdoc.'.format(doc_uuid)) + + tmp_file_path = os.path.join('/tmp/sdoc', doc_uuid, 'sdoc_asset/') # used to store asset files and json from file_server + tmp_zip_path = os.path.join('/tmp/sdoc', doc_uuid, 'zip_file') + '.zip' # zip path of zipped xxx.zip + + logger.info('Clear tmp dirs and files before prepare.') + export_sdoc_clear_tmp_files_and_dirs(tmp_file_path, tmp_zip_path) + os.makedirs(tmp_file_path, exist_ok=True) + + try: + download_link = get_seadoc_download_link(uuid_map, is_inner=True) + resp = requests.get(download_link) + file_obj = io.BytesIO(resp.content) + with open(os.path.join(tmp_file_path, 'content.json') , 'wb') as f: + f.write(file_obj.read()) + except Exception as e: + logger.error('prepare sdoc failed. ERROR: {}'.format(e)) + raise Exception('prepare sdoc failed. ERROR: {}'.format(e)) + + # 2. get images folder, images could be empty + parent_path = '/images/sdoc/' + doc_uuid + '/' + images_dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_path) + if images_dir_id: + logger.info('Create images folder.') + try: + export_sdoc_prepare_images_folder( + repo_id, doc_uuid, images_dir_id, username) + except Exception as e: + logger.warning('create images folder failed. ERROR: {}'.format(e)) + + logger.info('Make zip file for download...') + try: + shutil.make_archive('/tmp/sdoc/' + doc_uuid + '/zip_file', "zip", root_dir=tmp_file_path) + except Exception as e: + logger.error('make zip failed. ERROR: {}'.format(e)) + raise Exception('make zip failed. ERROR: {}'.format(e)) + logger.info('Create /tmp/sdoc/{}/zip_file.zip success!'.format(doc_uuid)) + return tmp_zip_path diff --git a/seahub/views/file.py b/seahub/views/file.py index aafc4224c65..d010c93290e 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -504,6 +504,13 @@ def view_lib_file(request, repo_id, path): if parse_repo_perm(permission).can_download is False: raise Http404 + # redirect to sdoc export + filetype, fileext = get_file_type_and_ext(filename) + if filetype == SEADOC: + file_uuid = get_seadoc_file_uuid(repo, path) + file_url = reverse('seadoc_export', args=[file_uuid]) + return HttpResponseRedirect(file_url) + operation = 'download' if dl else 'view' token = seafile_api.get_fileserver_access_token( repo_id, file_id, operation, username,