1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-31 22:57:47 +00:00

convert markdown and sdoc (#5636)

This commit is contained in:
JoinTyang 2023-09-14 14:36:58 +08:00 committed by GitHub
parent 34a7318725
commit e7d32fd4d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 208 additions and 8 deletions

View File

@ -57,6 +57,7 @@ const propTypes = {
onItemRename: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onDirentClick: PropTypes.func.isRequired,
isAllItemSelected: PropTypes.bool.isRequired,
onAllItemSelected: PropTypes.func.isRequired,
@ -217,6 +218,7 @@ class DirColumnView extends React.Component {
onItemRename={this.props.onItemRename}
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
onDirentClick={this.props.onDirentClick}
updateDirent={this.props.updateDirent}
isAllItemSelected={this.props.isAllItemSelected}

View File

@ -19,6 +19,7 @@ const propTypes = {
onItemDelete: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onRenameNode: PropTypes.func.isRequired,
isGroupOwnedRepo: PropTypes.bool.isRequired,
userPerm: PropTypes.string,
@ -73,6 +74,7 @@ class DirGridView extends React.Component {
onItemDelete={this.props.onItemDelete}
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
isDirentListLoading={this.props.isDirentListLoading}
updateDirent={this.props.updateDirent}
onRenameNode={this.props.onRenameNode}

View File

@ -36,6 +36,7 @@ const propTypes = {
selectedDirentList: PropTypes.array.isRequired,
onItemsMove: PropTypes.func.isRequired,
onItemsCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onItemsDelete: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func,
showDirentDetail: PropTypes.func.isRequired,
@ -96,6 +97,7 @@ class DirListView extends React.Component {
selectedDirentList={this.props.selectedDirentList}
onItemsMove={this.props.onItemsMove}
onItemsCopy={this.props.onItemsCopy}
onItemConvert={this.props.onItemConvert}
onItemsDelete={this.props.onItemsDelete}
onAddFile={this.props.onAddFile}
onAddFolder={this.props.onAddFolder}

View File

@ -33,6 +33,7 @@ const propTypes = {
onAddFile: PropTypes.func,
onItemDelete: PropTypes.func,
onItemCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
onRenameNode: PropTypes.func.isRequired,
onItemClick: PropTypes.func.isRequired,
@ -115,6 +116,11 @@ class DirentGridView extends React.Component {
this.props.onItemDelete(currentObject);
};
onItemConvert = (currentObject, e, dstType) => {
e.nativeEvent.stopImmediatePropagation(); //for document event
this.props.onItemConvert(currentObject, dstType);
}
onMenuItemClick = (operation, currentObject, event) => {
hideMenu();
switch(operation) {
@ -136,6 +142,12 @@ class DirentGridView extends React.Component {
case 'Copy':
this.onItemCopyToggle();
break;
case 'Convert to Markdown':
this.onItemConvert(currentObject, event, 'markdown');
break;
case 'Convert to sdoc':
this.onItemConvert(currentObject, event, 'sdoc');
break;
case 'Tags':
this.onEditFileTagToggle();
break;

View File

@ -35,6 +35,7 @@ const propTypes = {
onItemRename: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onDirentClick: PropTypes.func.isRequired,
updateDirent: PropTypes.func.isRequired,
showImagePopup: PropTypes.func.isRequired,
@ -268,6 +269,12 @@ class DirentListItem extends React.Component {
case 'Lock':
this.onLockItem();
break;
case 'Convert to Markdown':
this.onItemConvert(event, 'markdown');
break;
case 'Convert to sdoc':
this.onItemConvert(event, 'sdoc');
break;
case 'Mark as draft':
this.onMarkAsDraft();
break;
@ -299,6 +306,12 @@ class DirentListItem extends React.Component {
}
};
onItemConvert = (e, dstType)=> {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation(); //for document event
this.props.onItemConvert(this.props.dirent, dstType);
}
onEditFileTagToggle = () => {
this.setState({
isEditFileTagShow: !this.state.isEditFileTagShow

View File

@ -42,6 +42,7 @@ const propTypes = {
selectedDirentList: PropTypes.array.isRequired,
onItemsMove: PropTypes.func.isRequired,
onItemsCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onItemsDelete: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func,
enableDirPrivateShare: PropTypes.bool.isRequired,
@ -620,6 +621,7 @@ class DirentListView extends React.Component {
onItemRename={this.onItemRename}
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
updateDirent={this.props.updateDirent}
isItemFreezed={this.state.isItemFreezed}
freezeItem={this.freezeItem}

View File

@ -69,6 +69,7 @@ const propTypes = {
onItemCopy: PropTypes.func.isRequired,
onAddFolder: PropTypes.func.isRequired,
onAddFile: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
isDirentSelected: PropTypes.bool.isRequired,
isAllDirentSelected: PropTypes.bool.isRequired,
@ -229,6 +230,7 @@ class LibContentContainer extends React.Component {
onItemRename={this.props.onItemRename}
onItemMove={this.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
onDirentClick={this.onDirentClick}
updateDirent={this.props.updateDirent}
isAllItemSelected={this.props.isAllDirentSelected}
@ -264,6 +266,7 @@ class LibContentContainer extends React.Component {
onItemDelete={this.props.onItemDelete}
onItemMove={this.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
updateDirent={this.props.updateDirent}
onAddFolder={this.props.onAddFolder}
showDirentDetail={this.props.showDirentDetail}
@ -322,6 +325,7 @@ class LibContentContainer extends React.Component {
onItemRename={this.props.onItemRename}
onItemMove={this.onItemMove}
onItemCopy={this.props.onItemCopy}
onItemConvert={this.props.onItemConvert}
onDirentClick={this.onDirentClick}
updateDirent={this.props.updateDirent}
isAllItemSelected={this.props.isAllDirentSelected}

View File

@ -1236,6 +1236,32 @@ class LibContentView extends React.Component {
});
};
onConvertItem = (dirent, dstType) => {
let path = Utils.joinPath(this.state.path, dirent.name);
let repoID = this.props.repoID;
seafileAPI.convertFile(repoID, path, dstType).then((res) => {
let objName = res.data.obj_name;
let parentDir = res.data.parent_dir;
let file_size = res.data.size;
path = parentDir + '/' + objName;
let name = Utils.getFileName(path);
let parentPath = Utils.getDirName(path);
if (this.state.currentMode === 'column') {
this.updateMoveCopyTreeNode(parentPath);
}
this.loadDirentList(this.state.path);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
if (errMessage === gettext('Error')) {
let name = Utils.getFileName(path);
errMessage = gettext('Renaming {name} failed').replace('{name}', name);
}
toaster.danger(errMessage);
});
}
onDirentClick = (dirent) => {
let direntList = this.state.direntList.map(dirent => {
dirent.isSelected = false;
@ -2025,6 +2051,7 @@ class LibContentView extends React.Component {
onItemRename={this.onMainPanelItemRename}
onItemMove={this.onMoveItem}
onItemCopy={this.onCopyItem}
onItemConvert={this.onConvertItem}
onAddFolder={this.onAddFolder}
onAddFile={this.onAddFile}
onFileTagChanged={this.onFileTagChanged}

View File

@ -16,6 +16,8 @@ const TextTranslation = {
'OPEN_VIA_CLIENT' : {key : 'Open via Client', value : gettext('Open via Client')},
'LOCK' : {key : 'Lock', value : gettext('Lock')},
'UNLOCK' : {key : 'Unlock', value : gettext('Unlock')},
'CONVERT_TO_MARKDOWN' : {key : 'Convert to Markdown', value : gettext('Convert to Markdown')},
'CONVERT_TO_SDOC' : {key : 'Convert to sdoc', value : gettext('Convert to sdoc')},
'MARK_AS_DRAFT' : {key : 'Mark as draft', value : gettext('Mark as draft')},
'UNMARK_AS_DRAFT' : {key : 'Unmark as draft', value : gettext('Unmark as draft')},
'COMMENT' : {key : 'Comment', value : gettext('Comment')},

View File

@ -1,4 +1,4 @@
import { mediaUrl, gettext, serviceURL, siteRoot, isPro, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableOnlyoffice } from './constants';
import { mediaUrl, gettext, serviceURL, siteRoot, isPro, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableOnlyoffice, enableSeadoc } from './constants';
import TextTranslation from './text-translation';
import React from 'react';
import toaster from '../components/toast';
@ -528,7 +528,7 @@ export const Utils = {
getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) {
let list = [];
const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, MARK_AS_DRAFT, UNMARK_AS_DRAFT,
HISTORY, ACCESS_LOG, PROPERTIES, OPEN_VIA_CLIENT, ONLYOFFICE_CONVERT } = TextTranslation;
HISTORY, ACCESS_LOG, PROPERTIES, OPEN_VIA_CLIENT, ONLYOFFICE_CONVERT, CONVERT_TO_MARKDOWN, CONVERT_TO_SDOC } = TextTranslation;
const permission = dirent.permission;
const { isCustomPermission, customPermission } = Utils.getUserPermission(permission);
@ -595,6 +595,19 @@ export const Utils = {
}
list.push('Divider');
}
if ((permission == 'rw' || permission == 'cloud-edit') && enableSeadoc) {
if (dirent.name.endsWith('.md')) {
list.push(CONVERT_TO_SDOC);
}
if (dirent.name.endsWith('.sdoc')) {
list.push(CONVERT_TO_MARKDOWN);
}
}
if (permission == 'rw') {
if (Utils.isSdocFile(dirent.name)) {
if (dirent.is_sdoc_draft) {
list.push(UNMARK_AS_DRAFT);
@ -602,6 +615,7 @@ export const Utils = {
list.push(MARK_AS_DRAFT);
}
}
list.push('Divider');
/*
if (enableFileComment) {
list.push(COMMENT);

View File

@ -5,6 +5,7 @@ import json
import logging
import posixpath
import requests
from pathlib import Path
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
@ -20,18 +21,20 @@ from seahub.api2.utils import api_error
from seahub.utils import check_filename_with_rename, is_pro_version, \
gen_inner_file_upload_url, is_valid_dirent_name, normalize_file_path, \
normalize_dir_path, get_file_type_and_ext
normalize_dir_path, get_file_type_and_ext, check_filename_or_rename
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.views import check_folder_permission
from seahub.utils.file_op import check_file_lock, if_locked_by_online_office
from seahub.views.file import can_preview_file, can_edit_file
from seahub.constants import PERMISSION_READ_WRITE
from seahub.utils.repo import parse_repo_perm, is_repo_admin, is_repo_owner
from seahub.utils.file_types import MARKDOWN, TEXT, SEADOC
from seahub.utils.file_types import MARKDOWN, TEXT, SEADOC, MARKDOWN_SUPPORT_CONVERT_TYPES, SDOC_SUPPORT_CONVERT_TYPES
from seahub.tags.models import FileUUIDMap
from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocCommentReply
from seahub.base.models import FileComment
from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, OFFICE_TEMPLATE_ROOT
from seahub.api2.endpoints.utils import convert_file
from seahub.seadoc.utils import get_seadoc_file_uuid
from seahub.drafts.models import Draft
from seahub.drafts.utils import is_draft_file, get_file_draft
@ -152,8 +155,8 @@ class FileView(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
operation = operation.lower()
if operation not in ('create', 'rename', 'move', 'copy', 'revert'):
error_msg = "operation can only be 'create', 'rename', 'move', 'copy' or 'revert'."
if operation not in ('create', 'rename', 'move', 'copy', 'revert', 'convert'):
error_msg = "operation can only be 'create', 'rename', 'move', 'copy', 'convert' or 'revert'."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
@ -523,6 +526,80 @@ class FileView(APIView):
return Response({'success': True})
if operation == 'convert':
dst_type = request.data.get('dst_type')
extension = Path(path).suffix
if extension not in ['.md', '.sdoc']:
error_msg = 'path invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if (extension == '.md' and dst_type not in MARKDOWN_SUPPORT_CONVERT_TYPES) or \
(extension == '.sdoc' and dst_type not in SDOC_SUPPORT_CONVERT_TYPES):
error_msg = 'dst_type invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
try:
file_id = seafile_api.get_file_id_by_path(repo_id, path)
except SearpcError as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
if not file_id:
error_msg = 'File %s not found.' % path
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web is False:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# check file lock
try:
is_locked, locked_by_me = check_file_lock(repo_id, path, 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 is_locked and not locked_by_me:
error_msg = _("File is locked")
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
filename = os.path.basename(path)
if extension == '.md':
src_type = 'markdown'
filename = filename[:-2] + 'sdoc'
elif extension == '.sdoc':
src_type = 'sdoc'
filename = filename[:-4] + 'md'
new_file_name = check_filename_or_rename(repo_id, parent_dir, filename)
new_file_path = posixpath.join(parent_dir, new_file_name)
download_token = seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username)
obj_id = json.dumps({'parent_dir': parent_dir})
upload_token = seafile_api.get_fileserver_access_token(repo_id, obj_id, 'upload-link', username,
use_onetime=True)
doc_uuid = get_seadoc_file_uuid(repo, path)
try:
resp = convert_file(path, username, doc_uuid, download_token, upload_token, src_type, dst_type)
if resp.status_code == 500:
logger.error('convert file error status: %s body: %s', resp.status_code, resp.text)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
file_info = self.get_file_info(username, repo_id, new_file_path)
return Response(file_info)
def put(self, request, repo_id, format=None):
""" Currently only support lock, unlock, refresh-lock file.

View File

@ -4,6 +4,9 @@ import datetime
import time
import urllib.request, urllib.parse, urllib.error
import logging
import requests
import jwt
from urllib.parse import urljoin
from rest_framework import status
@ -11,9 +14,9 @@ from seaserv import ccnet_api, seafile_api
from pysearpc import SearpcError
from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname, \
email2contact_email
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
from seahub.utils import get_log_events_by_time, is_pro_version, is_org_context
from seahub.settings import SECRET_KEY, FILE_CONVERTER_SERVER_URL
try:
from seahub.settings import MULTI_TENANCY
@ -210,3 +213,26 @@ def get_user_quota_usage_and_total(email, org_id=''):
quota_usage = -1
quota_total = -1
return quota_usage, quota_total
def gen_headers():
payload = {'exp': int(time.time()) + 300, }
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return {"Authorization": "Token %s" % token}
def convert_file(path, username, doc_uuid, download_token, upload_token, src_type, dst_type):
headers = gen_headers()
params = {
'path': path,
'username': username,
'doc_uuid': doc_uuid,
'download_token': download_token,
'upload_token': upload_token,
'src_type': src_type,
'dst_type': dst_type,
}
url = urljoin(FILE_CONVERTER_SERVER_URL, '/api/v1/file-convert/')
resp = requests.post(url, json=params, headers=headers, timeout=30)
return resp

View File

@ -623,6 +623,8 @@ SHOW_LOGOUT_ICON = False
PRIVACY_POLICY_LINK = ''
TERMS_OF_SERVICE_LINK = ''
FILE_CONVERTER_SERVER_URL = 'http://127.0.0.1:8888'
# For security consideration, please set to match the host/domain of your site, e.g., ALLOWED_HOSTS = ['.example.com'].
# Please refer https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts for details.
ALLOWED_HOSTS = ['*']

View File

@ -334,6 +334,10 @@ def get_no_duplicate_obj_name(obj_name, exist_obj_names):
i += 1
def check_filename_with_rename(repo_id, parent_dir, obj_name):
exist_obj_names = list_obj_names_in_dir(repo_id, parent_dir)
return get_no_duplicate_obj_name(obj_name, exist_obj_names)
def list_obj_names_in_dir(repo_id, parent_dir):
cmmts = seafile_api.get_commit_list(repo_id, 0, 1)
latest_commit = cmmts[0] if cmmts else None
if not latest_commit:
@ -343,6 +347,13 @@ def check_filename_with_rename(repo_id, parent_dir, obj_name):
latest_commit.id, parent_dir)
exist_obj_names = [dirent.obj_name for dirent in dirents]
return exist_obj_names
def check_filename_or_rename(repo_id, parent_dir, obj_name):
exist_obj_names = list_obj_names_in_dir(repo_id, parent_dir)
if obj_name not in exist_obj_names:
return obj_name
return get_no_duplicate_obj_name(obj_name, exist_obj_names)
def get_user_repos(username, org_id=None):

View File

@ -10,3 +10,7 @@ AUDIO = 'Audio'
SPREADSHEET = 'SpreadSheet'
XMIND = 'XMind'
SEADOC = 'SDoc'
MARKDOWN_SUPPORT_CONVERT_TYPES = ['sdoc']
SDOC_SUPPORT_CONVERT_TYPES = ['markdown']