From 757497bd5aae2c63d1e9cc80bee291dec6c26017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A2=E4=B9=90=E9=A9=AC?= <38058090+SkywalkerSpace@users.noreply.github.com> Date: Mon, 19 Jun 2023 17:35:30 +0800 Subject: [PATCH] renameSdocHistory (#5500) * renameSdocHistory * sdoc NewFileHistoryView * add SeadocHistoryName --- .../sdoc-file-history/history-version.js | 42 ++++- frontend/src/pages/sdoc-file-history/index.js | 6 +- .../src/pages/sdoc-file-history/side-panel.js | 141 +++++++++------- seahub/seadoc/apis.py | 153 +++++++++++++++++- seahub/seadoc/models.py | 33 ++++ seahub/seadoc/urls.py | 3 +- seahub/settings.py | 1 + sql/mysql.sql | 10 ++ 8 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 seahub/seadoc/models.py diff --git a/frontend/src/pages/sdoc-file-history/history-version.js b/frontend/src/pages/sdoc-file-history/history-version.js index 2831a72863..2c51f3a30e 100644 --- a/frontend/src/pages/sdoc-file-history/history-version.js +++ b/frontend/src/pages/sdoc-file-history/history-version.js @@ -1,11 +1,15 @@ +import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap'; import { gettext, filePath } from '../../utils/constants'; import URLDecorator from '../../utils/url-decorator'; +import Rename from '../../components/rename'; import '../../css/history-record-item.css'; +moment.locale(window.app.config.lang); + class HistoryVersion extends React.Component { constructor(props) { @@ -13,18 +17,19 @@ class HistoryVersion extends React.Component { this.state = { isShowOperationIcon: false, isMenuShow: false, + isRenameShow: false, }; } onMouseEnter = () => { const { currentVersion, historyVersion } = this.props; - if (currentVersion.commitId === historyVersion.commitId) return; + if (currentVersion.commit_id === historyVersion.commit_id) return; this.setState({ isShowOperationIcon: true }); } onMouseLeave = () => { const { currentVersion, historyVersion } = this.props; - if (currentVersion.commitId === historyVersion.commitId) return; + if (currentVersion.commit_id === historyVersion.commit_id) return; this.setState({ isShowOperationIcon: false }); } @@ -35,7 +40,7 @@ class HistoryVersion extends React.Component { onClick = () => { this.setState({ isShowOperationIcon: false }); const { currentVersion, historyVersion } = this.props; - if (currentVersion.commitId === historyVersion.commitId) return; + if (currentVersion.commit_id === historyVersion.commit_id) return; this.props.onSelectHistoryVersion(historyVersion); } @@ -50,15 +55,30 @@ class HistoryVersion extends React.Component { onItemCopy = () => { const { historyVersion } = this.props; + historyVersion.ctime_format = moment(historyVersion.ctime).format('YYYY-MM-DD HH:mm'); this.props.onCopy(historyVersion); } + toggleRename = () => { + this.setState({isRenameShow: !this.state.isRenameShow}); + } + + onRenameConfirm = (newName) => { + const { obj_id } = this.props.historyVersion; + this.props.renameHistoryVersion(obj_id, newName); + this.toggleRename(); + } + + onRenameCancel = () => { + this.toggleRename(); + } + render() { const { currentVersion, historyVersion } = this.props; if (!currentVersion || !historyVersion) return null; - const { ctime, commitId, creatorName, revFileId } = historyVersion; - const isHighlightItem = commitId === currentVersion.commitId; - const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: revFileId }); + const { ctime, commit_id, creator_name, obj_id, name} = historyVersion; + const isHighlightItem = commit_id === currentVersion.commit_id; + const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: obj_id }); return (
  • -
    {ctime}
    + {this.state.isRenameShow ? + + :
    {name}
    + } +
    {moment(ctime).format('YYYY-MM-DD HH:mm')}
    - {creatorName} + {creator_name}
    @@ -86,6 +110,7 @@ class HistoryVersion extends React.Component { {(this.props.index !== 0) && {gettext('Restore')}} {gettext('Download')} {(this.props.index !== 0) && {gettext('Copy')}} + {gettext('Rename')}
    @@ -101,6 +126,7 @@ HistoryVersion.propTypes = { onSelectHistoryVersion: PropTypes.func.isRequired, onRestore: PropTypes.func.isRequired, onCopy: PropTypes.func.isRequired, + renameHistoryVersion: PropTypes.func.isRequired, }; export default HistoryVersion; diff --git a/frontend/src/pages/sdoc-file-history/index.js b/frontend/src/pages/sdoc-file-history/index.js index fddb3b787b..0d13168be5 100644 --- a/frontend/src/pages/sdoc-file-history/index.js +++ b/frontend/src/pages/sdoc-file-history/index.js @@ -50,12 +50,12 @@ class SdocFileHistory extends React.Component { onSelectHistoryVersion = (currentVersion, lastVersion) => { this.setState({ isLoading: true, currentVersion }); - seafileAPI.getFileRevision(historyRepoID, currentVersion.commitId, currentVersion.path).then(res => { + seafileAPI.getFileRevision(historyRepoID, currentVersion.commit_id, currentVersion.path).then(res => { return seafileAPI.getFileContent(res.data); }).then(res => { const currentVersionContent = res.data; if (lastVersion) { - seafileAPI.getFileRevision(historyRepoID, lastVersion.commitId, lastVersion.path).then(res => { + seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => { return seafileAPI.getFileContent(res.data); }).then(res => { const lastVersionContent = res.data; @@ -84,7 +84,7 @@ class SdocFileHistory extends React.Component { const { currentVersionContent } = this.state; this.setState({ isLoading: true }, () => { localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + ''); - seafileAPI.getFileRevision(historyRepoID, lastVersion.commitId, lastVersion.path).then(res => { + seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => { return seafileAPI.getFileContent(res.data); }).then(res => { const lastVersionContent = res.data; diff --git a/frontend/src/pages/sdoc-file-history/side-panel.js b/frontend/src/pages/sdoc-file-history/side-panel.js index 6befd3d4b2..392ca27a67 100644 --- a/frontend/src/pages/sdoc-file-history/side-panel.js +++ b/frontend/src/pages/sdoc-file-history/side-panel.js @@ -2,15 +2,16 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Loading from '../../components/loading'; -import { gettext, filePath, historyRepoID } from '../../utils/constants'; +import { gettext, historyRepoID, PER_PAGE } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; -import FileHistory from '../../models/file-history'; import { Utils } from '../../utils/utils'; import editUtilities from '../../utils/editor-utilities'; import toaster from '../../components/toast'; import HistoryVersion from './history-version'; import Switch from '../../components/common/switch'; +const { docUuid } = window.fileHistory.pageOptions; + class SidePanel extends Component { constructor(props) { @@ -19,85 +20,104 @@ class SidePanel extends Component { isLoading: true, historyVersions: [], errorMessage: '', + currentPage: 1, + hasMore: false, + fileOwner: '', + isReloadingData: false, }; - this.init(); } componentDidMount() { - this.listHistoryVersions(historyRepoID, filePath, this.nextCommit, (historyVersion, lastHistoryVersion) => { - this.props.onSelectHistoryVersion(historyVersion, lastHistoryVersion); + seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => { + let historyList = res.data; + if (historyList.length === 0) { + this.setState({isLoading: false}); + throw Error('there has an error in server'); + } + this.initResultState(res.data); }); } - init = () => { - this.hasMore = true; - this.nextCommit = ''; - this.filePath = ''; - this.oldFilePath = ''; + refershFileList() { + seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => { + this.initResultState(res.data); + }); } - listHistoryVersions = (repoID, filePath, commit, callback) => { - seafileAPI.listOldFileHistoryRecords(repoID, filePath, commit).then((res) => { - let historyData = res.data; - if (!historyData) { - this.setState({ isLoading: false, errorMessage: 'There is an error in server.' }); - return; - } - this.updateHistoryVersions(historyData, callback); + initResultState(result) { + if (result.histories.length) { + this.setState({ + historyVersions: result.histories, + currentPage: result.page, + hasMore: result.total_count > (PER_PAGE * this.state.currentPage), + isLoading: false, + fileOwner: result.histories[0].creator_email, + }); + this.props.onSelectHistoryVersion(result.histories[0], result.histories[1]); + } + } + + updateResultState(result) { + if (result.histories.length) { + this.setState({ + historyVersions: [...this.state.historyVersions, ...result.histories], + currentPage: result.page, + hasMore: result.total_count > (PER_PAGE * this.state.currentPage), + isLoading: false, + fileOwner: result.histories[0].creator_email + }); + } + } + + loadMore = () => { + if (!this.state.isReloadingData) { + let currentPage = this.state.currentPage + 1; + this.setState({ + currentPage: currentPage, + isReloadingData: true, + }); + seafileAPI.listSdocHistory(docUuid, currentPage, PER_PAGE).then(res => { + this.updateResultState(res.data); + this.setState({isReloadingData: false}); + }); + } + } + + renameHistoryVersion = (objID, newName) => { + seafileAPI.renameSdocHistory(docUuid, objID, newName).then((res) => { + this.setState({ + historyVersions: this.state.historyVersions.map(item => { + if (item.obj_id == objID) { + item.name = newName; + } + return item; + }) + }); }).catch(error => { const errorMessage = Utils.getErrorMsg(error, true); this.setState({ isLoading: false, errorMessage: errorMessage }); }); } - updateHistoryVersions(result, callback) { - const dataCount = result.data ? result.data.length : 0; - this.nextCommit = result.next_start_commit || ''; - if (dataCount) { - const addedHistoryVersions = result.data.map(item => new FileHistory(item)); - this.filePath = addedHistoryVersions[dataCount - 1].path; - this.oldFilePath = addedHistoryVersions[dataCount - 1].revRenamedOldPath; - const historyVersions = [ ...this.state.historyVersions, ...addedHistoryVersions ]; - this.setState({ historyVersions: historyVersions, isLoading: false, errorMessage: '' }, () => { - callback && callback(historyVersions[0], historyVersions[1]); - }); - return; - } - if (this.nextCommit) { - this.listHistoryVersions(historyRepoID, filePath, this.nextCommit); - return; - } - this.hasMore = false; - this.setState({ isLoading: false, errorMessage: '' }); - } - onScrollHandler = (event) => { const clientHeight = event.target.clientHeight; const scrollHeight = event.target.scrollHeight; const scrollTop = event.target.scrollTop; const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); - if (isBottom && this.hasMore && this.nextCommit) { + if (isBottom && this.state.hasMore) { this.loadMore(); } } - loadMore = () => { - if (this.state.isLoading) return; - this.setState({ isLoading: true }, () => { - const currentFilePath = this.oldFilePath || this.filePath; - this.listHistoryVersions(historyRepoID, currentFilePath, this.nextCommit); - }); - } - - restoreVersion = (historyVersion) => { - const { commitId, path } = historyVersion; - editUtilities.revertFile(path, commitId).then(res => { + restoreVersion = (currentItem) => { + const { commit_id, path } = currentItem; + editUtilities.revertFile(path, commit_id).then(res => { if (res.data.success) { - this.init(); - this.setState({ isLoading: true, historyVersions: [], errorMessage: '' } , () => { - this.listHistoryVersions(historyRepoID, filePath); - }); + this.setState({isLoading: true}); + this.refershFileList(); } + let message = gettext('Successfully restored.'); + toaster.success(message); }).catch(error => { const errorMessage = Utils.getErrorMsg(error, true); toaster.danger(gettext(errorMessage)); @@ -111,13 +131,13 @@ class SidePanel extends Component { return; } const { historyVersions } = this.state; - const historyVersionIndex = historyVersions.findIndex(item => item.commitId === historyVersion.commitId); + const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === historyVersion.commit_id); this.props.onSelectHistoryVersion(historyVersion, historyVersions[historyVersionIndex + 1]); } copyHistoryFile = (historyVersion) => { - const { path, revFileId, ctime } = historyVersion; - seafileAPI.sdocCopyHistoryFile(historyRepoID, path, revFileId, ctime).then(res => { + const { path, obj_id, ctime_format } = historyVersion; + seafileAPI.sdocCopyHistoryFile(historyRepoID, path, obj_id, ctime_format).then(res => { let message = gettext('Successfully copied %(name)s.'); let filename = res.data.file_name; message = message.replace('%(name)s', filename); @@ -157,13 +177,14 @@ class SidePanel extends Component { {historyVersions.map((historyVersion, index) => { return ( ); })} @@ -179,7 +200,7 @@ class SidePanel extends Component { onShowChanges = () => { const { isShowChanges, currentVersion } = this.props; const { historyVersions } = this.state; - const historyVersionIndex = historyVersions.findIndex(item => item.commitId === currentVersion.commitId); + const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === currentVersion.commit_id); const lastVersion = historyVersions[historyVersionIndex + 1]; this.props.onShowChanges(!isShowChanges, lastVersion); } diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index d281724d07..c26a36acac 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -3,6 +3,7 @@ import json import logging import requests import posixpath +from datetime import datetime from rest_framework.response import Response from rest_framework.views import APIView @@ -24,11 +25,17 @@ from seahub.seadoc.utils import is_valid_seadoc_access_token, get_seadoc_upload_ gen_seadoc_image_parent_path, get_seadoc_asset_upload_link, get_seadoc_asset_download_link, \ can_access_seadoc_asset from seahub.utils.file_types import SEADOC, IMAGE -from seahub.utils import get_file_type_and_ext, normalize_file_path, PREVIEW_FILEEXT, \ +from seahub.utils import get_file_type_and_ext, normalize_file_path, PREVIEW_FILEEXT, get_file_history, \ gen_inner_file_get_url, gen_inner_file_upload_url from seahub.tags.models import FileUUIDMap from seahub.utils.error_msg import file_type_error_msg from seahub.utils.repo import parse_repo_perm +from seahub.utils.file_revisions import get_file_revisions_within_limit +from seahub.seadoc.models import SeadocHistoryName +from seahub.avatar.templatetags.avatar_tags import api_avatar_url +from seahub.base.templatetags.seahub_tags import email2nickname, \ + email2contact_email +from seahub.utils.timeutils import utc_datetime_to_isoformat_timestr, timestamp_to_isoformat_timestr logger = logging.getLogger(__name__) @@ -353,3 +360,147 @@ class SeadocCopyHistoryFile(APIView): 'file_name': new_file_name, 'file_path': new_file_path, }) + + +class SeadocHistory(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def _get_new_file_history_info(self, ent, avatar_size, name_dict): + info = {} + creator_name = ent.op_user + url, is_default, date_uploaded = api_avatar_url(creator_name, avatar_size) + info['creator_avatar_url'] = url + info['creator_email'] = creator_name + info['creator_name'] = email2nickname(creator_name) + info['creator_contact_email'] = email2contact_email(creator_name) + info['ctime'] = utc_datetime_to_isoformat_timestr(ent.timestamp) + info['size'] = ent.size + info['obj_id'] = ent.file_id + info['commit_id'] = ent.commit_id + info['old_path'] = ent.old_path if hasattr(ent, 'old_path') else '' + info['path'] = ent.path + info['name'] = name_dict.get(ent.file_id, '') + return info + + def get(self, request, file_uuid): + """list history, same as NewFileHistoryView + """ + 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) + + repo_id = uuid_map.repo_id + username = request.user.username + path = posixpath.join(uuid_map.parent_path, uuid_map.filename) + + # permission check + if not check_folder_permission(request, repo_id, path): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, 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) + commit_id = repo.head_cmmt_id + + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + page = int(request.GET.get('page', 1)) + per_page = int(request.GET.get('per_page', 25)) + except ValueError: + avatar_size = 32 + page = 1 + per_page = 25 + + # Don't use seafile_api.get_file_id_by_path() + # if path parameter is `rev_renamed_old_path`. + # seafile_api.get_file_id_by_path() will return None. + file_id = seafile_api.get_file_id_by_commit_and_path(repo_id, + commit_id, path) + if not file_id: + error_msg = 'File %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # get repo history limit + try: + history_limit = seafile_api.get_repo_history_limit(repo_id) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + start = (page - 1) * per_page + count = per_page + try: + file_revisions, total_count = get_file_history(repo_id, path, start, count, history_limit) + except Exception as e: + 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: + name_queryset = SeadocHistoryName.objects.list_by_obj_ids( + doc_uuid=file_uuid, obj_id_list=obj_id_list) + name_dict = {item.obj_id: item.name for item in name_queryset} + data = [self._get_new_file_history_info(ent, avatar_size, name_dict) for ent in file_revisions] + result = { + "histories": data, + "page": page, + "total_count": total_count + } + return Response(result) + + def post(self, request, file_uuid): + """rename history + """ + username = request.user.username + obj_id = request.data.get('obj_id', '') + new_name = request.data.get('new_name', '') + if not obj_id: + error_msg = 'obj_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if not new_name: + error_msg = 'new_name invalid.' + 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 = 'seadoc uuid %s not found.' % file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_id = uuid_map.repo_id + username = request.user.username + path = posixpath.join(uuid_map.parent_path, uuid_map.filename) + + # permission check + if not check_folder_permission(request, repo_id, path): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, 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) + + token = seafile_api.get_fileserver_access_token(repo_id, + obj_id, 'download', username) + if not token: + error_msg = 'history %s not found.' % obj_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # main + SeadocHistoryName.objects.update_name(file_uuid, obj_id, new_name) + + return Response({ + 'obj_id': obj_id, + 'name': new_name, + }) diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py new file mode 100644 index 0000000000..f84c2bc6da --- /dev/null +++ b/seahub/seadoc/models.py @@ -0,0 +1,33 @@ +from django.db import models + + +class SeadocHistoryNameManager(models.Manager): + def update_name(self, doc_uuid, obj_id, name): + if self.filter(doc_uuid=doc_uuid, obj_id=obj_id).exists(): + obj = self.filter(doc_uuid=doc_uuid, obj_id=obj_id).update(name=name) + else: + obj = self.create(doc_uuid=doc_uuid, obj_id=obj_id, name=name) + return obj + + def list_by_obj_ids(self, doc_uuid, obj_id_list): + return self.filter(doc_uuid=doc_uuid, obj_id__in=obj_id_list) + + +class SeadocHistoryName(models.Model): + doc_uuid = models.CharField(max_length=36, db_index=True) + obj_id = models.CharField(max_length=40) + name = models.CharField(max_length=255) + + objects = SeadocHistoryNameManager() + + class Meta: + db_table = 'history_name' + unique_together = ('doc_uuid', 'obj_id') + + def to_dict(self): + return { + 'id': self.pk, + 'doc_uuid': self.doc_uuid, + 'obj_id': self.obj_id, + 'name': self.name, + } diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index e6d1bb5d59..956956aee4 100644 --- a/seahub/seadoc/urls.py +++ b/seahub/seadoc/urls.py @@ -1,6 +1,6 @@ from django.urls import re_path from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocUploadFile, \ - SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile + SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory urlpatterns = [ re_path(r'^access-token/(?P[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'), @@ -10,4 +10,5 @@ urlpatterns = [ re_path(r'^upload-image/(?P[-0-9a-f]{36})/$', SeadocUploadImage.as_view(), name='seadoc_upload_image'), re_path(r'^download-image/(?P[-0-9a-f]{36})/(?P.*)$', SeadocDownloadImage.as_view(), name='seadoc_download_image'), re_path(r'^copy-history-file/(?P[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'), + re_path(r'^history/(?P[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'), ] diff --git a/seahub/settings.py b/seahub/settings.py index 223e3da8af..1b8f9fe551 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -275,6 +275,7 @@ INSTALLED_APPS = [ 'seahub.organizations', 'seahub.krb5_auth', 'seahub.django_cas_ng', + 'seahub.seadoc', ] diff --git a/sql/mysql.sql b/sql/mysql.sql index 9274c276ee..be7c8e69b4 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1375,3 +1375,13 @@ CREATE TABLE `organizations_orgadminsettings` ( UNIQUE KEY `organizations_orgadminsettings_org_id_key_a01cc7de_uniq` (`org_id`,`key`), KEY `organizations_orgadminsettings_org_id_4f70d186` (`org_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `history_name` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `doc_uuid` varchar(36) NOT NULL, + `obj_id` varchar(40) NOT NULL, + `name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `history_name_doc_uuid` (`doc_uuid`), + UNIQUE KEY `history_name_doc_uuid_obj_id` (`doc_uuid`, `obj_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;