diff --git a/frontend/src/pages/wiki2/index.js b/frontend/src/pages/wiki2/index.js index f3d192aceaf..d55a0a1af94 100644 --- a/frontend/src/pages/wiki2/index.js +++ b/frontend/src/pages/wiki2/index.js @@ -84,11 +84,15 @@ class Wiki extends Component { }); }; + updateWikiConfig = (wikiConfig) => { + this.setState({ + config: new WikiConfig(wikiConfig || {}), + }); + }; + saveWikiConfig = (wikiConfig, onSuccess, onError) => { wikiAPI.updateWiki2Config(wikiId, JSON.stringify(wikiConfig)).then(res => { - this.setState({ - config: new WikiConfig(wikiConfig || {}), - }); + this.updateWikiConfig(wikiConfig); onSuccess && onSuccess(); }).catch((error) => { let errorMsg = Utils.getErrorMsg(error); @@ -187,6 +191,10 @@ class Wiki extends Component { } const { pages } = config; const currentPage = PageUtils.getPageById(pages, pageId); + if (!currentPage) { + callback && callback(); + return; + } const { path, id, name, docUuid } = currentPage; if (path !== this.state.path) this.showPage(pageId, path); this.onCloseSide(); @@ -225,6 +233,7 @@ class Wiki extends Component { onCloseSide={this.onCloseSide} config={this.state.config} saveWikiConfig={this.saveWikiConfig} + updateWikiConfig={this.updateWikiConfig} setCurrentPage={this.setCurrentPage} currentPageId={this.state.currentPageId} onUpdatePage={this.onUpdatePage} diff --git a/frontend/src/pages/wiki2/side-panel.js b/frontend/src/pages/wiki2/side-panel.js index 08516352703..075e6f8f93a 100644 --- a/frontend/src/pages/wiki2/side-panel.js +++ b/frontend/src/pages/wiki2/side-panel.js @@ -27,6 +27,7 @@ const propTypes = { isLoading: PropTypes.bool.isRequired, config: PropTypes.object.isRequired, saveWikiConfig: PropTypes.func.isRequired, + updateWikiConfig: PropTypes.func.isRequired, setCurrentPage: PropTypes.func.isRequired, currentPageId: PropTypes.string, onUpdatePage: PropTypes.func.isRequired, @@ -75,18 +76,16 @@ class SidePanel extends Component { }; duplicatePage = async (fromPageConfig, successCallback, errorCallback) => { - const { config } = this.props; - const { name, from_page_id } = fromPageConfig; - const { navigation, pages } = config; - const fromPage = PageUtils.getPageById(pages, from_page_id); - const newPageId = generateUniqueId(navigation); - let newPageConfig = { - ...fromPage, - id: newPageId, - name, - }; - const newPage = new Page({ ...newPageConfig }); - this.addPage(newPage, this.current_folder_id, successCallback, errorCallback); + const { from_page_id } = fromPageConfig; + wikiAPI.duplicateWiki2Page(wikiId, from_page_id).then(res => { + const newConfig = JSON.parse(res.data.wiki_config); + this.props.updateWikiConfig(newConfig); + successCallback && successCallback(); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + errorCallback && errorCallback(); + }); }; addPage = (page, parentId, successCallback, errorCallback, jumpToNewPage = true) => { diff --git a/frontend/src/pages/wiki2/top-nav/index.jsx b/frontend/src/pages/wiki2/top-nav/index.jsx index 2c3708b7012..2e45cbd0055 100644 --- a/frontend/src/pages/wiki2/top-nav/index.jsx +++ b/frontend/src/pages/wiki2/top-nav/index.jsx @@ -21,8 +21,10 @@ function getPaths(navigation, currentPageId, pages) { } if (node.children) { node.children.forEach(child => { - child._path = newPath; - runNode(child); + if (child) { + child._path = newPath; + runNode(child); + } }); } } diff --git a/frontend/src/pages/wiki2/wiki-nav/page-utils.js b/frontend/src/pages/wiki2/wiki-nav/page-utils.js index 65121f446b4..4c1ee58b79f 100644 --- a/frontend/src/pages/wiki2/wiki-nav/page-utils.js +++ b/frontend/src/pages/wiki2/wiki-nav/page-utils.js @@ -163,7 +163,6 @@ export default class PageUtils { }); } - // move page into folder or page(已解决) static movePage(navigation, moved_page_id, target_page_id, source_id, target_id, move_position) { let movedPage = null; function _cutPageRecursion(item, page_id) { diff --git a/frontend/src/pages/wiki2/wiki-nav/pages/page-dropdownmenu.js b/frontend/src/pages/wiki2/wiki-nav/pages/page-dropdownmenu.js index a9e1ea50ca2..7da29da58d8 100644 --- a/frontend/src/pages/wiki2/wiki-nav/pages/page-dropdownmenu.js +++ b/frontend/src/pages/wiki2/wiki-nav/pages/page-dropdownmenu.js @@ -73,16 +73,8 @@ export default class PageDropdownMenu extends Component { duplicatePage = () => { const { page, folderId } = this.props; - const { id: from_page_id, name } = page; - let duplicateCount = 1; - let newName = name + '(copy)'; - while (this.pageNameMap[newName]) { - newName = `${name}(copy${duplicateCount})`; - duplicateCount++; - } - const onsuccess = () => {}; this.props.onSetFolderId(folderId); - this.props.duplicatePage({ name: newName, from_page_id }, onsuccess, this.duplicatePageFailure); + this.props.duplicatePage({ from_page_id: page.id }, () => {}, this.duplicatePageFailure); }; duplicatePageFailure = () => { diff --git a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js index f83ce303424..6df3b79b488 100644 --- a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js +++ b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js @@ -125,6 +125,7 @@ class PageItem extends Component { }; renderPage = (page, index, pagesLength, isOnlyOnePage) => { + if (!page) return; const { isEditMode, pages, folderId, pathStr } = this.props; const id = page.id; if (!pages.find(item => item.id === id)) return; diff --git a/frontend/src/utils/wiki-api.js b/frontend/src/utils/wiki-api.js index 5e5ca072401..3ed1222d924 100644 --- a/frontend/src/utils/wiki-api.js +++ b/frontend/src/utils/wiki-api.js @@ -176,10 +176,13 @@ class WikiAPI { return this.req.get(url); } - createWiki2Page(wikiId, pageName) { + createWiki2Page(wikiId, pageName, currentId) { const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/pages/'; let form = new FormData(); form.append('page_name', pageName); + if (currentId) { + form.append('current_id', currentId); + } return this._sendPostRequest(url, form); } @@ -206,6 +209,13 @@ class WikiAPI { return this.req.put(url, params); } + duplicateWiki2Page(wikiId, pageId) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/duplicate-page/'; + let form = new FormData(); + form.append('page_id', pageId); + return this._sendPostRequest(url, form); + } + } let wikiAPI = new WikiAPI(); diff --git a/seahub/api2/endpoints/wiki2.py b/seahub/api2/endpoints/wiki2.py index 25a339200e6..d3d6ba20215 100644 --- a/seahub/api2/endpoints/wiki2.py +++ b/seahub/api2/endpoints/wiki2.py @@ -8,6 +8,7 @@ import time import uuid import urllib.request, urllib.error, urllib.parse +from copy import deepcopy from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -25,7 +26,8 @@ from seahub.wiki2.models import Wiki2 as Wiki from seahub.wiki2.utils import is_valid_wiki_name, can_edit_wiki, get_wiki_dirs_by_path, \ get_wiki_config, WIKI_PAGES_DIR, WIKI_CONFIG_PATH, WIKI_CONFIG_FILE_NAME, is_group_wiki, \ - check_wiki_admin_permission, check_wiki_permission, get_page_ids_in_folder + check_wiki_admin_permission, check_wiki_permission, get_page_ids_in_folder, get_all_wiki_ids, \ + get_and_gen_page_nav_by_id, get_current_level_page_ids, save_wiki_config from seahub.utils import is_org_context, get_user_repos, gen_inner_file_get_url, gen_file_upload_url, \ normalize_dir_path, is_pro_version, check_filename_with_rename, is_valid_dirent_name, get_no_duplicate_obj_name from seahub.views import check_folder_permission @@ -33,7 +35,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname from seahub.utils.file_op import check_file_lock, ONLINE_OFFICE_LOCK_OWNER, if_locked_by_online_office from seahub.utils.repo import parse_repo_perm -from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token +from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token, copy_sdoc_images_with_sdoc_uuid from seahub.settings import SEADOC_SERVER_URL, ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \ ENCRYPTED_LIBRARY_VERSION from seahub.seadoc.sdoc_server_api import SdocServerAPI @@ -324,27 +326,11 @@ def put(self, request, wiki_id): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - repo_id = wiki.repo_id - obj_id = json.dumps({'parent_dir': WIKI_CONFIG_PATH}) - - dir_id = seafile_api.get_dir_id_by_path(repo_id, WIKI_CONFIG_PATH) - if not dir_id: - seafile_api.mkdir_with_parents(repo_id, '/', WIKI_CONFIG_PATH, username) - - token = seafile_api.get_fileserver_access_token( - repo_id, obj_id, 'upload-link', username, use_onetime=False) - if not token: - return None - upload_link = gen_file_upload_url(token, 'upload-api') - upload_link = upload_link + '?replace=1' - - files = { - 'file': (WIKI_CONFIG_FILE_NAME, wiki_config) - } - data = {'parent_dir': WIKI_CONFIG_PATH, 'relative_path': '', 'replace': 1} - resp = requests.post(upload_link, files=files, data=data) - if not resp.ok: - logger.error(resp.text) + # save config + try: + save_wiki_config(wiki, username, wiki_config) + except Exception as e: + logger.exception(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) @@ -427,14 +413,15 @@ def post(self, request, wiki_id): error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) - folder_id = request.data.get('folder_id', None) + current_id = request.data.get('current_id', None) wiki_config = get_wiki_config(repo_id, request.user.username) navigation = wiki_config.get('navigation', []) - if not folder_id: + if not current_id: page_ids = {element.get('id') for element in navigation if element.get('type') != 'folder'} else: - page_ids = get_page_ids_in_folder(navigation, folder_id) + page_ids = [] + get_current_level_page_ids(navigation, current_id, page_ids) pages = wiki_config.get('pages', []) exist_page_names = [page.get('name') for page in pages if page.get('id') in page_ids] @@ -613,3 +600,126 @@ def delete(self, request, wiki_id, page_id): logger.error(e) return Response({'success': True}) + + +class Wiki2DuplicatePageView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def post(self, request, wiki_id): + page_id = request.data.get('page_id', None) + + if not page_id: + error_msg = 'page_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + wiki = Wiki.objects.get(id=wiki_id) + except Wiki.DoesNotExist: + error_msg = "Wiki not found." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + username = request.user.username + if not check_wiki_permission(wiki, username): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + repo_id = wiki.repo_id + + # 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) + + wiki_config = get_wiki_config(repo_id, username) + navigation = wiki_config.get('navigation', []) + pages = wiki_config.get('pages', []) + + page_ids = [] + get_current_level_page_ids(navigation, page_id, page_ids) + + current_exist_page_names = [page.get('name') for page in pages if page.get('id') in page_ids] + id_set = get_all_wiki_ids(navigation) # get all id for generate different page id + + # old page to new page + old_to_new = {} + get_and_gen_page_nav_by_id(id_set, navigation, page_id, old_to_new) + + # page_id not exist in wiki + if not old_to_new: + error_msg = 'page %s not found.' % page_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + src_repo_id = repo_id + dst_repo_id = repo_id + new_pages = deepcopy(pages) + for page in pages: + src_dirents = [] + dst_dirents = [] + old_page_id = page.get('id') + new_page_id = old_to_new.get(old_page_id) + if not new_page_id: + continue + + page_name = page.get('name') + dst_sdoc_uuid = uuid.uuid4() + + src_path = page.get('path') + src_file_name = os.path.basename(src_path) + + new_file_name = src_file_name + parent_dir = os.path.join(WIKI_PAGES_DIR, str(dst_sdoc_uuid)) + path = os.path.join(parent_dir, new_file_name) + seafile_api.mkdir_with_parents(repo_id, '/', parent_dir.strip('/'), request.user.username) + + new_page_name = page_name + if old_page_id == page_id: + # only need rename current level page name + new_page_name = get_no_duplicate_obj_name(page_name, current_exist_page_names) + + new_page = { + 'id': new_page_id, + 'name': new_page_name, + 'path': path, + 'icon': '', + 'docUuid': str(dst_sdoc_uuid) + } + new_pages.append(new_page) + + src_dirent = src_file_name + dst_dirent = src_dirent + src_dirents.append(src_dirent) + dst_dirents.append(dst_dirent) + + src_doc_uuid = page.get('docUuid') + src_parent_dir = os.path.join(WIKI_PAGES_DIR, str(src_doc_uuid)) + dst_parent_dir = parent_dir + + seafile_api.copy_file(src_repo_id, + src_parent_dir, + json.dumps(src_dirents), + dst_repo_id, + dst_parent_dir, + json.dumps(dst_dirents), + username=username, + need_progress=1, + synchronous=0 + ) + + FileUUIDMap.objects.create_fileuuidmap_by_uuid(dst_sdoc_uuid, dst_repo_id, parent_dir, dst_dirent, is_dir=False) + copy_sdoc_images_with_sdoc_uuid(src_repo_id, src_doc_uuid, dst_repo_id, dst_sdoc_uuid, username, is_async=False) + + wiki_config['pages'] = new_pages + wiki_config = json.dumps(wiki_config) + + # save config + try: + save_wiki_config(wiki, username, wiki_config) + except Exception as e: + logger.exception(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'wiki_config': wiki_config}) diff --git a/seahub/seadoc/utils.py b/seahub/seadoc/utils.py index bdd416bd709..289cc12203e 100644 --- a/seahub/seadoc/utils.py +++ b/seahub/seadoc/utils.py @@ -263,6 +263,34 @@ def copy_sdoc_images(src_repo_id, src_path, dst_repo_id, dst_path, username, is_ return +def copy_sdoc_images_with_sdoc_uuid(src_repo_id, src_file_uuid, dst_repo_id, dst_file_uuid, username, is_async=True): + src_image_parent_path = SDOC_IMAGES_DIR + src_file_uuid + '/' + src_dir_id = seafile_api.get_dir_id_by_path(src_repo_id, src_image_parent_path) + if not src_dir_id: + return + + dst_image_parent_path = gen_seadoc_image_parent_path( + dst_file_uuid, dst_repo_id, username=username) + image_dirents = seafile_api.list_dir_by_path(src_repo_id, src_image_parent_path) + image_names = [item.obj_name for item in image_dirents] + + if is_async: + need_progress=1 + synchronous=0 + else: + need_progress=0 + synchronous=1 + seafile_api.copy_file( + src_repo_id, src_image_parent_path, + json.dumps(image_names), + dst_repo_id, dst_image_parent_path, + json.dumps(image_names), + username=username, + need_progress=need_progress, synchronous=synchronous, + ) + return + + def move_sdoc_images(src_repo_id, src_path, dst_repo_id, dst_path, username, is_async=True): src_repo = seafile_api.get_repo(src_repo_id) src_file_uuid = get_seadoc_file_uuid(src_repo, src_path) diff --git a/seahub/urls.py b/seahub/urls.py index 23243fa46e4..4b95d598d44 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -204,7 +204,7 @@ from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \ LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken from seahub.wiki2.views import wiki_view -from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView +from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, Wiki2DuplicatePageView from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage from seahub.api2.endpoints.user_list import UserListView @@ -534,6 +534,7 @@ re_path(r'^api/v2.1/wiki2/(?P\d+)/config/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'), re_path(r'^api/v2.1/wiki2/(?P\d+)/pages/$', Wiki2PagesView.as_view(), name='api-v2.1-wiki2-pages'), re_path(r'^api/v2.1/wiki2/(?P\d+)/page/(?P[-0-9a-zA-Z]{4})/$', Wiki2PageView.as_view(), name='api-v2.1-wiki2-page'), + re_path(r'^api/v2.1/wiki2/(?P\d+)/duplicate-page/$', Wiki2DuplicatePageView.as_view(), name='api-v2.1-wiki2-duplicate-page'), ## user::drafts re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'), diff --git a/seahub/wiki2/utils.py b/seahub/wiki2/utils.py index cf3226a7214..017a1aa9bbb 100644 --- a/seahub/wiki2/utils.py +++ b/seahub/wiki2/utils.py @@ -7,10 +7,11 @@ import json import requests import posixpath +import random from seaserv import seafile_api from seahub.constants import PERMISSION_READ_WRITE -from seahub.utils import gen_inner_file_get_url +from seahub.utils import gen_inner_file_get_url, gen_file_upload_url from seahub.group.utils import is_group_admin, is_group_member logger = logging.getLogger(__name__) @@ -99,3 +100,110 @@ def get_page_ids_in_folder(navigation, folder_id): elif directory.get('type') == 'folder': navigation = directory.get('children', []) return get_page_ids_in_folder(navigation, folder_id) + + +def generator_base64_code(length=4): + possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789' + ids = random.sample(possible, length) + return ''.join(ids) + + +def get_all_wiki_ids(navigation): + id_set = set() + + def recurse_item(item): + id_set.add(item.get('id')) + children = item.get('children') + if children: + for child in children: + recurse_item(child) + + for nav in navigation: + recurse_item(nav) + return id_set + + +def gen_unique_id(id_set, length=4): + _id = generator_base64_code(length) + + while True: + if _id not in id_set: + return _id + _id = generator_base64_code(length) + + +def duplicate_children(id_set, children, old_to_new): + old_children = [] + for child in children: + old_id = child.get('id') + old_path = child.get('_path') + old_type = child.get('type') + sub_old_children = child.get('children', []) + new_id = gen_unique_id(id_set) + old_to_new[old_id] = new_id + id_set.add(new_id) + new_child = { + 'id': new_id, + '_path': old_path, + 'type': old_type, + 'children': duplicate_children(id_set, sub_old_children, old_to_new), + } + + old_children.append(new_child) + return old_children + + +def get_and_gen_page_nav_by_id(id_set, navigation, page_id, old_to_new): + for nav in navigation: + if nav.get('type') == 'page' and nav.get('id') == page_id: + new_id = gen_unique_id(id_set) + id_set.add(new_id) + old_to_new[page_id] = new_id + children = nav.get('children', []) + new_nav = { + 'id': new_id, + 'type': 'page', + '_path': nav.get('_path'), + 'children': duplicate_children(id_set, children, old_to_new), + } + navigation.append(new_nav) + return + else: + new_navigation = nav.get('children', []) + get_and_gen_page_nav_by_id(id_set, new_navigation, page_id, old_to_new) + + +def get_current_level_page_ids(navigation, page_id, ids=[]): + for item in navigation: + if item.get('id') == page_id: + ids.extend([child.get('id') for child in navigation if child.get('type') == 'page']) + return + else: + children = item.get('children') or [] + get_current_level_page_ids(children, page_id, ids) + + +def save_wiki_config(wiki, username, wiki_config): + repo_id = wiki.repo_id + obj_id = json.dumps({'parent_dir': WIKI_CONFIG_PATH}) + + dir_id = seafile_api.get_dir_id_by_path(repo_id, WIKI_CONFIG_PATH) + if not dir_id: + seafile_api.mkdir_with_parents(repo_id, '/', WIKI_CONFIG_PATH, username) + + token = seafile_api.get_fileserver_access_token( + repo_id, obj_id, 'upload-link', username, use_onetime=False) + + if not token: + raise Exception('upload token invalid') + + upload_link = gen_file_upload_url(token, 'upload-api') + upload_link = upload_link + '?replace=1' + + files = { + 'file': (WIKI_CONFIG_FILE_NAME, wiki_config) + } + data = {'parent_dir': WIKI_CONFIG_PATH, 'relative_path': '', 'replace': 1} + resp = requests.post(upload_link, files=files, data=data) + if not resp.ok: + raise Exception(resp.text)