diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7410d5847e..6caba4daf9 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -55,6 +55,7 @@
"react-select": "5.9.0",
"react-transition-group": "4.4.5",
"reactstrap": "9.2.3",
+ "socket.io-client": "^4.8.1",
"svg-sprite-loader": "^6.0.11",
"svgo-loader": "^3.0.1",
"unified": "^7.0.0",
@@ -5638,42 +5639,6 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
- "node_modules/@seafile/sdoc-editor/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@seafile/sdoc-editor/node_modules/engine.io-client": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
- "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
- "dependencies": {
- "@socket.io/component-emitter": "~3.1.0",
- "debug": "~4.3.1",
- "engine.io-parser": "~5.2.1",
- "ws": "~8.17.1",
- "xmlhttprequest-ssl": "~2.1.1"
- }
- },
- "node_modules/@seafile/sdoc-editor/node_modules/engine.io-parser": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
- "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
"node_modules/@seafile/sdoc-editor/node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -5686,32 +5651,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@seafile/sdoc-editor/node_modules/socket.io-client": {
- "version": "4.8.1",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
- "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
- "dependencies": {
- "@socket.io/component-emitter": "~3.1.0",
- "debug": "~4.3.2",
- "engine.io-client": "~6.6.1",
- "socket.io-parser": "~4.2.4"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@seafile/sdoc-editor/node_modules/socket.io-parser": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
- "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
- "dependencies": {
- "@socket.io/component-emitter": "~3.1.0",
- "debug": "~4.3.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
"node_modules/@seafile/sdoc-editor/node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz",
@@ -5741,34 +5680,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/@seafile/sdoc-editor/node_modules/ws": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
- "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/@seafile/sdoc-editor/node_modules/xmlhttprequest-ssl": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
- "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/@seafile/seafile-calendar": {
"version": "0.0.28",
"resolved": "https://registry.npmmirror.com/@seafile/seafile-calendar/-/seafile-calendar-0.0.28.tgz",
@@ -12237,6 +12148,71 @@
"node": ">= 0.8"
}
},
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/engine.io-client": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-client/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.0",
"resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
@@ -27066,6 +27042,64 @@
"node": ">=0.10.0"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/sockjs": {
"version": "0.3.24",
"resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",
@@ -31257,6 +31291,14 @@
"node": ">=0.1"
}
},
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 2cf781ac78..72a8acb62e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,6 +50,7 @@
"react-select": "5.9.0",
"react-transition-group": "4.4.5",
"reactstrap": "9.2.3",
+ "socket.io-client": "^4.8.1",
"svg-sprite-loader": "^6.0.11",
"svgo-loader": "^3.0.1",
"unified": "^7.0.0",
diff --git a/frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js b/frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js
new file mode 100644
index 0000000000..d01269a55e
--- /dev/null
+++ b/frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js
@@ -0,0 +1,28 @@
+import axios from 'axios';
+
+class ExdrawServerApi {
+
+ constructor(options) {
+ this.server = options.exdrawServer;
+ this.docUuid = options.exdrawUuid;
+ this.accessToken = options.accessToken;
+ }
+
+ getSceneContent() {
+ const { server, docUuid, accessToken } = this;
+ const url = `${server}/api/v1/exdraw/${docUuid}/`;
+
+ return axios.get(url, { headers: { Authorization: `Token ${accessToken}` } });
+ }
+
+ saveSceneContent(content) {
+ const { server, docUuid, accessToken } = this;
+ const url = `${server}/api/v1/exdraw/${docUuid}/`;
+ const formData = new FormData();
+ formData.append('doc_content', content);
+
+ return axios.post(url, formData, { headers: { Authorization: `Token ${accessToken}` } });
+ }
+}
+
+export default ExdrawServerApi;
diff --git a/frontend/src/pages/excalidraw-editor/editor-api.js b/frontend/src/pages/excalidraw-editor/editor-api.js
index f2524cca2f..fb92916139 100644
--- a/frontend/src/pages/excalidraw-editor/editor-api.js
+++ b/frontend/src/pages/excalidraw-editor/editor-api.js
@@ -19,6 +19,12 @@ class EditorApi {
return seafileAPI.getFileContent(downLoadUrl);
});
};
+
+ getExdrawToken = () => {
+ return seafileAPI.getExdrawToken(repoID, filePath).then((res => {
+ return res.data.access_token;
+ }));
+ };
}
const editorApi = new EditorApi();
diff --git a/frontend/src/pages/excalidraw-editor/index.js b/frontend/src/pages/excalidraw-editor/index.js
index 28524de502..1e1c330b2b 100644
--- a/frontend/src/pages/excalidraw-editor/index.js
+++ b/frontend/src/pages/excalidraw-editor/index.js
@@ -6,23 +6,35 @@ import { gettext } from '../../utils/constants';
import toaster from '../../components/toast';
import { SAVE_INTERVAL_TIME } from './constants';
import { Utils } from '../../utils/utils';
+import ExdrawServerApi from './collab/exdraw-server-api';
import './index.css';
+const { docUuid, excalidrawServerUrl } = window.app.pageOptions;
+
const ExcaliEditor = () => {
const [fileContent, setFileContent] = useState(null);
const editorRef = useRef(null);
const isChangedRef = useRef(false);
const [isFetching, setIsFetching] = useState(true);
+ const exdrawServerConfigRef = useRef({
+ exdrawServer: '',
+ exdrawUuid: '',
+ accessToken: ''
+ });
useEffect(() => {
- editorApi.getFileContent().then(res => {
- if (res.data?.appState?.collaborators && !Array.isArray(res.data.appState.collaborators)) {
- // collaborators.forEach is not a function
- res.data['appState']['collaborators'] = [];
- }
- setFileContent(res.data);
- setIsFetching(false);
+ editorApi.getExdrawToken().then(res => {
+ exdrawServerConfigRef.current = {
+ exdrawServer: excalidrawServerUrl,
+ exdrawUuid: docUuid,
+ accessToken: res
+ };
+ const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current);
+ exdrawServerApi.getSceneContent().then(res => {
+ setFileContent(res.data);
+ setIsFetching(false);
+ });
});
onSetFavicon();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -31,7 +43,8 @@ const ExcaliEditor = () => {
const saveSceneContent = useCallback(async () => {
if (isChangedRef.current) {
try {
- await editorApi.saveContent(JSON.stringify(editorRef.current));
+ const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current);
+ await exdrawServerApi.saveSceneContent(JSON.stringify(editorRef.current));
isChangedRef.current = false;
toaster.success(gettext('Successfully saved'), { duration: 2 });
} catch {
diff --git a/frontend/src/pages/excalidraw-editor/simple-editor.js b/frontend/src/pages/excalidraw-editor/simple-editor.js
index 827f287478..77a19b466c 100644
--- a/frontend/src/pages/excalidraw-editor/simple-editor.js
+++ b/frontend/src/pages/excalidraw-editor/simple-editor.js
@@ -21,7 +21,6 @@ const SimpleEditor = ({
},
tools: { image: false },
};
-
const handleChange = () => {
const elements = excalidrawAPI.getSceneElements();
if (hasChanged(elements, prevElementsRef.current)) {
@@ -69,6 +68,22 @@ const SimpleEditor = ({
onChange={handleChange}
UIOptions={UIOptions}
langCode={langList[window.app.config.lang] || 'en'}
+ renderTopRightUI={() => {
+ return (
+
+ );
+ }}
>
diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js
index faa330c76f..b362d27c36 100644
--- a/frontend/src/utils/seafile-api.js
+++ b/frontend/src/utils/seafile-api.js
@@ -2222,6 +2222,21 @@ class SeafileAPI {
return this._sendPostRequest(url, formData);
}
+ sysAdminListFileTransferLogs(page, perPage) {
+ const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/';
+ let params = {
+ page: page,
+ per_page: perPage
+ };
+ return this.req.get(url, { params: params });
+ }
+
+ getExdrawToken(repoID, filePath) {
+ const url = `/api/v2.1/exdraw/access-token/${repoID}/?p=${filePath}`;
+
+ return this.req.get(url);
+ }
+
}
let seafileAPI = new SeafileAPI();
diff --git a/seahub/api2/endpoints/file.py b/seahub/api2/endpoints/file.py
index 65e2a0beac..1d7b9482dc 100644
--- a/seahub/api2/endpoints/file.py
+++ b/seahub/api2/endpoints/file.py
@@ -37,6 +37,7 @@ 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, sdoc_convert_to_docx
from seahub.seadoc.utils import get_seadoc_file_uuid
+from seahub.exdraw.utils import get_exdraw_file_uuid
from seaserv import seafile_api
from pysearpc import SearpcError
@@ -266,6 +267,9 @@ class FileView(APIView):
if new_file_name.endswith('.sdoc'):
doc_uuid = get_seadoc_file_uuid(repo, new_file_path)
file_info['doc_uuid'] = doc_uuid
+ if new_file_name.endswith('.exdraw'):
+ file_uuid = get_exdraw_file_uuid(repo, new_file_path)
+ file_info['file_uuid'] = file_uuid
return Response(file_info)
if operation == 'rename':
diff --git a/seahub/exdraw/__init__.py b/seahub/exdraw/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/exdraw/apis.py b/seahub/exdraw/apis.py
new file mode 100644
index 0000000000..eec94218dd
--- /dev/null
+++ b/seahub/exdraw/apis.py
@@ -0,0 +1,200 @@
+import os
+import logging
+import requests
+import posixpath
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+
+from seaserv import seafile_api
+
+from seahub.views import check_folder_permission
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.utils import api_error
+from seahub.api2.throttling import UserRateThrottle
+from seahub.exdraw.utils import is_valid_exdraw_access_token, get_exdraw_upload_link, get_exdraw_download_link, \
+ get_exdraw_file_uuid, gen_exdraw_access_token
+
+from seahub.utils.file_types import EXCALIDRAW
+from seahub.utils.file_op import if_locked_by_online_office
+from seahub.utils import get_file_type_and_ext, normalize_file_path, is_pro_version
+from seahub.tags.models import FileUUIDMap
+
+
+logger = logging.getLogger(__name__)
+
+
+class ExdrawAccessToken(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle, )
+
+ def get(self, request, repo_id):
+ username = request.user.username
+ # argument check
+ path = request.GET.get('p', None)
+ if not path:
+ error_msg = 'p invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ path = normalize_file_path(path)
+ parent_dir = os.path.dirname(path)
+ filename = os.path.basename(path)
+
+ filetype, fileext = get_file_type_and_ext(filename)
+ if filetype != EXCALIDRAW:
+ error_msg = 'exdraw file type %s invalid.' % filetype
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # resource check
+ 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)
+
+ try:
+ obj_id = seafile_api.get_file_id_by_path(repo_id, path)
+ 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 obj_id:
+ error_msg = 'File %s not found.' % path
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ permission = check_folder_permission(request, repo_id, parent_dir)
+ if not permission:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ file_uuid = get_exdraw_file_uuid(repo, path)
+ access_token = gen_exdraw_access_token(file_uuid, filename, username, permission=permission)
+
+ return Response({'access_token': access_token})
+
+
+class ExdrawUploadFile(APIView):
+ authentication_classes = ()
+ throttle_classes = (UserRateThrottle, )
+
+ def post(self, request, file_uuid):
+ # jwt permission check
+ auth = request.headers.get('authorization', '').split()
+ if not is_valid_exdraw_access_token(auth, file_uuid):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ file = request.FILES.get('file', None)
+ if not file:
+ error_msg = 'file not found.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid)
+ if not uuid_map:
+ error_msg = 'exdraw 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 != EXCALIDRAW:
+ error_msg = 'exdraw file type %s invalid.' % filetype
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename)
+ file_path = os.path.normpath(file_path)
+ file_id = seafile_api.get_file_id_by_path(uuid_map.repo_id, file_path)
+ if not file_id: # save file anyway
+ seafile_api.post_empty_file(
+ uuid_map.repo_id, uuid_map.parent_path, uuid_map.filename, '')
+
+ last_modify_user = request.POST.get('last_modify_user', '')
+ upload_link = get_exdraw_upload_link(uuid_map, last_modify_user)
+ if not upload_link:
+ error_msg = 'exdraw file %s not found.' % uuid_map.filename
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # update file
+ files = {'file': file}
+ data = {'filename': uuid_map.filename, 'target_file': file_path}
+ resp = requests.post(upload_link, files=files, data=data)
+ if not resp.ok:
+ logger.error('save exdraw failed %s, %s' % (file_uuid, resp.text))
+ return api_error(resp.status_code, resp.content)
+
+ return Response({'success': True})
+
+
+class ExdrawDownloadLink(APIView):
+ authentication_classes = ()
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, file_uuid):
+ # jwt permission check
+ auth = request.headers.get('authorization', '').split()
+ if not is_valid_exdraw_access_token(auth, file_uuid):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid)
+ if not uuid_map:
+ error_msg = 'exdraw 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 != EXCALIDRAW:
+ error_msg = 'exdraw file type %s invalid.' % filetype
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ download_link = get_exdraw_download_link(uuid_map)
+ if not download_link:
+ error_msg = 'exdraw file %s not found.' % uuid_map.filename
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ return Response({'download_link': download_link})
+
+
+class ExdrawEditorCallBack(APIView):
+ authentication_classes = ()
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request, file_uuid):
+ # jwt permission check
+ auth = request.headers.get('authorization', '').split()
+ if not is_valid_exdraw_access_token(auth, file_uuid):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # file info check
+ uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid)
+ if not uuid_map:
+ error_msg = 'exdraw 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 != EXCALIDRAW:
+ error_msg = 'exdraw file type %s invalid.' % filetype
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # currently only implement unlock file
+ exdraw_status = request.POST.get('status', '')
+ if exdraw_status != 'no_write':
+ error_msg = 'status invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # unlock file
+ repo_id = uuid_map.repo_id
+ file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename)
+ file_path = os.path.normpath(file_path)
+ try:
+ if is_pro_version() and if_locked_by_online_office(repo_id, file_path):
+ seafile_api.unlock_file(repo_id, file_path)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
diff --git a/seahub/exdraw/urls.py b/seahub/exdraw/urls.py
new file mode 100644
index 0000000000..1fdf9d8eea
--- /dev/null
+++ b/seahub/exdraw/urls.py
@@ -0,0 +1,10 @@
+from django.urls import re_path
+from .apis import ExdrawAccessToken, ExdrawDownloadLink, ExdrawUploadFile, ExdrawEditorCallBack
+
+
+urlpatterns = [
+ re_path(r'^access-token/(?P[-0-9a-f]{36})/$', ExdrawAccessToken.as_view(), name='exdraw_access_token'),
+ re_path(r'^upload-file/(?P[-0-9a-f]{36})/$', ExdrawUploadFile.as_view(), name='exdraw_upload_file'),
+ re_path(r'^download-link/(?P[-0-9a-f]{36})/$', ExdrawDownloadLink.as_view(), name='exdraw_download_link'),
+ re_path(r'^editor-status-callback/(?P[-0-9a-f]{36})/$', ExdrawEditorCallBack.as_view(), name='exdraw_editor_callback'),
+]
diff --git a/seahub/exdraw/utils.py b/seahub/exdraw/utils.py
new file mode 100644
index 0000000000..eacc5748ca
--- /dev/null
+++ b/seahub/exdraw/utils.py
@@ -0,0 +1,120 @@
+import os
+import jwt
+import json
+import time
+import logging
+import posixpath
+
+from seaserv import seafile_api
+
+from seahub.tags.models import FileUUIDMap
+from seahub.settings import EXCALIDRAW_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.auth import AUTHORIZATION_PREFIX
+from seahub.base.templatetags.seahub_tags import email2nickname
+from seahub.avatar.templatetags.avatar_tags import api_avatar_url
+from seahub.utils import uuid_str_to_36_chars
+
+logger = logging.getLogger(__name__)
+
+
+def gen_exdraw_access_token(file_uuid, filename, username, permission='rw'):
+ name = email2nickname(username)
+ url, is_default, date_uploaded = api_avatar_url(username)
+ access_token = jwt.encode({
+ 'file_uuid': file_uuid,
+ 'filename': filename,
+ 'username': username,
+ 'name': name,
+ 'avatar_url': url,
+ 'permission': permission,
+ 'exp': int(time.time()) + 86400 * 3, # 3 days
+ },
+ EXCALIDRAW_PRIVATE_KEY,
+ algorithm='HS256'
+ )
+ return access_token
+
+
+def is_valid_exdraw_access_token(auth, file_uuid, return_payload=False):
+ """
+ can decode a valid jwt payload
+ """
+ is_valid, payload = False, None
+ if not auth or auth[0].lower() not in AUTHORIZATION_PREFIX or len(auth) != 2:
+ return (is_valid, payload) if return_payload else is_valid
+
+ token = auth[1]
+ if not token or not file_uuid:
+ return (is_valid, payload) if return_payload else is_valid
+
+ try:
+ payload = jwt.decode(token, EXCALIDRAW_PRIVATE_KEY, algorithms=['HS256'])
+ except Exception as e:
+ logger.error('Failed to decode jwt: %s' % e)
+ is_valid = False
+ else:
+ file_uuid_in_payload = payload.get('file_uuid')
+
+ if not file_uuid_in_payload:
+ is_valid = False
+ elif uuid_str_to_36_chars(file_uuid_in_payload) != uuid_str_to_36_chars(file_uuid):
+ is_valid = False
+ else:
+ is_valid = True
+
+ if return_payload:
+ return is_valid, payload
+ return is_valid
+
+
+def get_exdraw_file_uuid(repo, path):
+ repo_id = repo.repo_id
+ if repo.is_virtual:
+ repo_id = repo.origin_repo_id
+ path = posixpath.join(repo.origin_path, path.strip('/'))
+
+ path = normalize_file_path(path)
+ parent_dir = os.path.dirname(path)
+ filename = os.path.basename(path)
+
+ uuid_map = FileUUIDMap.objects.get_or_create_fileuuidmap(
+ repo_id, parent_dir, filename, is_dir=False)
+
+ file_uuid = str(uuid_map.uuid) # 36 chars str
+ return file_uuid
+
+
+def get_exdraw_upload_link(uuid_map, last_modify_user=''):
+ repo_id = uuid_map.repo_id
+ parent_path = uuid_map.parent_path
+
+ obj_id = json.dumps({'online_office_update': True, 'parent_dir': parent_path})
+ token = seafile_api.get_fileserver_access_token(
+ repo_id, obj_id, 'update', last_modify_user, use_onetime=True)
+ if not token:
+ return None
+ upload_link = gen_file_upload_url(token, 'update-api')
+ return upload_link
+
+
+def get_exdraw_download_link(uuid_map, is_inner=False):
+ repo_id = uuid_map.repo_id
+ parent_path = uuid_map.parent_path
+ filename = uuid_map.filename
+ file_path = posixpath.join(parent_path, filename)
+
+ obj_id = seafile_api.get_file_id_by_path(repo_id, file_path)
+ if not obj_id:
+ return None
+ token = seafile_api.get_fileserver_access_token(
+ repo_id, obj_id, 'view', '', use_onetime=False)
+ if not token:
+ return None
+
+ if is_inner:
+ download_link = gen_inner_file_get_url(token, filename)
+ else:
+ download_link = gen_file_get_url(token, filename)
+
+ return download_link
diff --git a/seahub/seadoc/utils.py b/seahub/seadoc/utils.py
index eefd79bb38..7c3f928e33 100644
--- a/seahub/seadoc/utils.py
+++ b/seahub/seadoc/utils.py
@@ -3,7 +3,6 @@ import io
import jwt
import json
import time
-import uuid
import logging
import posixpath
import shutil
@@ -22,26 +21,13 @@ from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.seadoc.models import SeadocRevision
from seahub.seadoc.settings import SDOC_REVISIONS_DIR, SDOC_IMAGES_DIR
+from seahub.utils import uuid_str_to_36_chars
logger = logging.getLogger(__name__)
ZSDOC = 'sdoczip'
-def uuid_str_to_32_chars(file_uuid):
- if len(file_uuid) == 36:
- return uuid.UUID(file_uuid).hex
- else:
- return file_uuid
-
-
-def uuid_str_to_36_chars(file_uuid):
- if len(file_uuid) == 32:
- return str(uuid.UUID(file_uuid))
- else:
- return file_uuid
-
-
def gen_seadoc_access_token(file_uuid, filename, username, permission='rw', default_title=None):
name = email2nickname(username)
url, is_default, date_uploaded = api_avatar_url(username)
diff --git a/seahub/settings.py b/seahub/settings.py
index cb8a107d80..0c604f4a68 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -287,6 +287,7 @@ INSTALLED_APPS = [
'seahub.django_cas_ng',
'seahub.seadoc',
'seahub.subscription',
+ 'seahub.exdraw',
]
@@ -962,11 +963,12 @@ ENABLE_WHITEBOARD = False
##########################
ENABLE_EXCALIDRAW = False
+EXCALIDRAW_SERVER_URL = 'http://127.0.0.1:7070'
######################################
# Settings for notification server #
######################################
-
+
NOTIFICATION_SERVER_URL = os.environ.get('NOTIFICATION_SERVER_URL', '')
############################
@@ -1173,7 +1175,7 @@ JWT_PRIVATE_KEY = os.environ.get('JWT_PRIVATE_KEY', '') or JWT_PRIVATE_KEY
# For database conf., now Seafile only support MySQL, skip for other engine
if 'default' in DATABASES and 'mysql' in DATABASES['default'].get('ENGINE', ''):
-
+
## For dtable_db
_rewrite_db_env_key_map = {
'HOST': 'SEAFILE_MYSQL_DB_HOST',
@@ -1245,6 +1247,10 @@ SEADOC_PRIVATE_KEY = JWT_PRIVATE_KEY
SEADOC_SERVER_URL = os.environ.get('SEADOC_SERVER_URL', '') or SEADOC_SERVER_URL
FILE_CONVERTER_SERVER_URL = SEADOC_SERVER_URL.rstrip('/') + '/converter'
+if os.environ.get('ENABLE_EXCALIDRAW', ''):
+ ENABLE_EXCALIDRAW = os.environ.get('ENABLE_EXCALIDRAW', '').lower() == 'true'
+EXCALIDRAW_PRIVATE_KEY = JWT_PRIVATE_KEY
+
if os.environ.get('SITE_ROOT', ''):
SITE_ROOT = os.environ.get('SITE_ROOT', '')
SEAFILE_SERVER_PROTOCOL = os.environ.get('SEAFILE_SERVER_PROTOCOL', '')
diff --git a/seahub/templates/excalidraw_file_view_react.html b/seahub/templates/excalidraw_file_view_react.html
index 17805eb736..7f0f7eba73 100644
--- a/seahub/templates/excalidraw_file_view_react.html
+++ b/seahub/templates/excalidraw_file_view_react.html
@@ -12,6 +12,7 @@ docName: '{{ filename|escapejs }}',
docUuid: '{{ file_uuid }}',
lang: '{{ lang }}',
rawPath: '{{ raw_path|escapejs }}',
+excalidrawServerUrl: '{{excalidraw_server_url}}'
{% endblock %}
{% block render_bundle %}
diff --git a/seahub/urls.py b/seahub/urls.py
index 95175444c7..ced7cdf767 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -586,7 +586,7 @@ urlpatterns = [
re_path(r'^api/v2.1/wiki2/search/$', WikiSearch.as_view(), name='api-v2.1-wiki2-search'),
re_path(r'^api/v2.1/convert-wiki/$', WikiConvertView.as_view(), name='api-v2.1-wiki-convert'),
## user::drafts
-
+
## user::activities
re_path(r'^api/v2.1/activities/$', ActivitiesView.as_view(), name='api-v2.1-acitvity'),
@@ -1049,6 +1049,10 @@ if getattr(settings, 'ENABLE_SEADOC', False):
re_path(r'^api/v2.1/seadoc/', include('seahub.seadoc.urls')),
]
+if getattr(settings, 'ENABLE_EXCALIDRAW', False):
+ urlpatterns += [
+ re_path(r'^api/v2.1/exdraw/', include('seahub.exdraw.urls')),
+ ]
if getattr(settings, 'CLIENT_SSO_VIA_LOCAL_BROWSER', False):
urlpatterns += [
diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py
index 1fc06bcb93..9cfe252f8b 100644
--- a/seahub/utils/__init__.py
+++ b/seahub/utils/__init__.py
@@ -78,7 +78,7 @@ else:
def is_pro_version():
return getattr(seahub.settings, 'IS_PRO_VERSION', False) is True
-
+
def is_cluster_mode():
cfg = configparser.ConfigParser()
if 'SEAFILE_CENTRAL_CONF_DIR' in os.environ:
@@ -738,7 +738,7 @@ if EVENTS_CONFIG_FILE:
events = seafevents_api.get_file_audit_events(session, email, org_id, repo_id, start, limit)
return events if events else None
-
+
def get_log_events_by_users_and_repos(type, emails, repo_ids, start, limit):
with _get_seafevents_session() as session:
events = seafevents_api.get_events_by_users_and_repos(session, type, emails, repo_ids, start, limit)
@@ -1497,3 +1497,17 @@ def transfer_repo(repo_id, new_owner, is_share, org_id=None):
seafile_api.transfer_repo_to_group(repo_id, group_id, PERMISSION_READ_WRITE)
else:
seafile_api.set_repo_owner(repo_id, new_owner)
+
+
+def uuid_str_to_32_chars(file_uuid):
+ if len(file_uuid) == 36:
+ return uuid.UUID(file_uuid).hex
+ else:
+ return file_uuid
+
+
+def uuid_str_to_36_chars(file_uuid):
+ if len(file_uuid) == 32:
+ return str(uuid.UUID(file_uuid))
+ else:
+ return file_uuid
diff --git a/seahub/views/file.py b/seahub/views/file.py
index f214b9219a..0b84cd97ff 100644
--- a/seahub/views/file.py
+++ b/seahub/views/file.py
@@ -69,6 +69,7 @@ from seahub.views import check_folder_permission, \
from seahub.utils.repo import is_repo_owner, parse_repo_perm, is_repo_admin
from seahub.group.utils import is_group_member
from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token, is_seadoc_revision
+from seahub.exdraw.utils import get_exdraw_file_uuid
from seahub.seadoc.models import SeadocRevision
import seahub.settings as settings
@@ -78,7 +79,7 @@ from seahub.settings import FILE_ENCODING_LIST, FILE_PREVIEW_MAX_SIZE, \
SHARE_LINK_FORCE_USE_PASSWORD, SHARE_LINK_PASSWORD_STRENGTH_LEVEL, \
SHARE_LINK_EXPIRE_DAYS_DEFAULT, ENABLE_SHARE_LINK_REPORT_ABUSE, SEADOC_SERVER_URL, \
ENABLE_METADATA_MANAGEMENT, BAIDU_MAP_KEY, GOOGLE_MAP_KEY, GOOGLE_MAP_ID, ENABLE_MULTIPLE_OFFICE_SUITE, \
- OFFICE_SUITE_LIST
+ OFFICE_SUITE_LIST, EXCALIDRAW_SERVER_URL
from seahub.constants import PERMISSION_INVISIBLE
# wopi
@@ -787,11 +788,13 @@ def view_lib_file(request, repo_id, path):
return render(request, template, return_dict)
elif filetype == EXCALIDRAW:
-
+ file_uuid = get_exdraw_file_uuid(repo, path)
+ return_dict['file_uuid'] = file_uuid
return_dict['protocol'] = request.is_secure() and 'https' or 'http'
return_dict['domain'] = get_current_site(request).domain
return_dict['serviceUrl'] = get_service_url().rstrip('/')
return_dict['language_code'] = get_language()
+ return_dict['excalidraw_server_url'] = EXCALIDRAW_SERVER_URL
return_dict['share_link_expire_days_Default'] = SHARE_LINK_EXPIRE_DAYS_DEFAULT
return_dict['share_link_expire_days_min'] = SHARE_LINK_EXPIRE_DAYS_MIN
return_dict['share_link_expire_days_max'] = SHARE_LINK_EXPIRE_DAYS_MAX