1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-18 00:00:00 +00:00

can add and modify property (#6297)

* can add and modify property

* clear extend property code
This commit is contained in:
JoinTyang
2024-07-05 15:13:26 +08:00
committed by GitHub
parent 182b0f278f
commit 9916ac43e1
29 changed files with 297 additions and 931 deletions

View File

@@ -1,67 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import toaster from '../toast';
import { Utils } from '../../utils/utils';
import Loading from '../loading';
import '../../css/apply-folder-properties.css';
const propTypes = {
toggle: PropTypes.func,
repoID: PropTypes.string,
path: PropTypes.string
};
class ConfirmApplyFolderPropertiesDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
submitting: false
};
}
submit = () => {
const { repoID, path } = this.props;
this.setState({ submitting: true });
seafileAPI.applyFolderExtendedProperties(repoID, path).then(() => {
toaster.success(gettext('Successfully applied the properties.'));
this.props.toggle();
}).catch(error => {
let errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
this.setState({ submitting: false });
});
};
render() {
const { submitting } = this.state;
return (
<Modal isOpen={true} toggle={this.props.toggle} className="apply-properties-dialog">
<ModalHeader toggle={this.props.toggle}>
{gettext('Apply properties')}
</ModalHeader>
<ModalBody>
<p>
{gettext('Are you sure you want to apply the properties to all the files inside the folder?')}
</p>
</ModalBody>
<ModalFooter>
<Button color='secondary' onClick={this.props.toggle} disabled={submitting}>{gettext('Cancel')}</Button>
<Button color='primary' className='flex-shrink-0 apply-properties' disabled={submitting} onClick={this.submit}>
{submitting ? (<Loading />) : (<>{gettext('Submit')}</>)}
</Button>
</ModalFooter>
</Modal>
);
}
}
ConfirmApplyFolderPropertiesDialog.propTypes = propTypes;
export default ConfirmApplyFolderPropertiesDialog;

View File

@@ -2,18 +2,19 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
import isHotkey from 'is-hotkey';
import { zIndexes, DIALOG_MAX_HEIGHT, EXTRA_ATTRIBUTES_COLUMN_TYPE } from '../../../constants';
import { zIndexes, DIALOG_MAX_HEIGHT } from '../../../constants';
import { gettext } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import { getSelectColumnOptions, getValidColumns } from '../../../utils/extra-attributes';
import { getValidColumns } from '../../../utils/extra-attributes';
import Column from './column';
import Loading from '../../loading';
import toaster from '../../toast';
import metadataAPI from '../../../metadata/api';
import './index.css';
class ExtraAttributesDialog extends Component {
class ExtraMetadataAttributesDialog extends Component {
constructor(props) {
super(props);
@@ -71,64 +72,43 @@ class ExtraAttributesDialog extends Component {
}, 1);
};
getFormatUpdateData = (update = {}) => {
const { columns } = this.state;
const updateData = {};
for (let key in update) {
const column = columns.find(column => column.key === key);
if (column && column.editable) {
const { type, name } = column;
const value = update[key];
if (type === EXTRA_ATTRIBUTES_COLUMN_TYPE.SINGLE_SELECT) {
const options = getSelectColumnOptions(column);
const option = options.find(item => item.id === value);
updateData[name] = option ? option.name : '';
} else {
updateData[column.name] = update[key];
}
}
}
return updateData;
};
getData = () => {
const { repoID, filePath } = this.props;
seafileAPI.getFileExtendedProperties(repoID, filePath).then(res => {
const { repoID, filePath, direntType } = this.props;
let dirName = Utils.getDirName(filePath);
let fileName = Utils.getFileName(filePath);
let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1);
if (!parentDir.startsWith('/')) {
parentDir = '/' + parentDir;
}
metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => {
const { row, metadata, editable_columns } = res.data;
this.isExist = Boolean(row._id);
this.setState({ row: row, columns: getValidColumns(metadata, editable_columns, this.isEmptyFile), isLoading: false, errorMsg: '' });
}).catch(error => {
const errorMsg =Utils.getErrorMsg(error);
const errorMsg = Utils.getErrorMsg(error);
this.setState({ isLoading: false, errorMsg });
});
};
createData = (data) => {
const { repoID, filePath } = this.props;
seafileAPI.newFileExtendedProperties(repoID, filePath, data).then(res => {
this.isExist = true;
const { row } = res.data;
this.setState({ row: row, isLoading: false, errorMsg: '' });
}).catch(error => {
const errorMsg =Utils.getErrorMsg(error);
toaster.danger(gettext(errorMsg));
});
};
updateData = (update, column) => {
const newRow = { ...this.state.row, ...update };
this.setState({ row: newRow }, () => {
const data = this.getFormatUpdateData(update);
const { repoID, filePath } = this.props;
let newValue = update[column.key];
let recordID = this.state.row._id;
if (this.isExist) {
seafileAPI.updateFileExtendedProperties(repoID, filePath, data).then(res => {
metadataAPI.updateMetadataRecord(repoID, recordID, column.name, newValue).then(res => {
this.setState({ update: {}, row: res.data.row });
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(gettext(errorMsg));
});
} else {
this.createData(data);
// this.createData(data);
}
});
};
@@ -244,7 +224,7 @@ class ExtraAttributesDialog extends Component {
}
}
ExtraAttributesDialog.propTypes = {
ExtraMetadataAttributesDialog.propTypes = {
repoID: PropTypes.string,
filePath: PropTypes.string,
direntType: PropTypes.string,
@@ -252,4 +232,4 @@ ExtraAttributesDialog.propTypes = {
onToggle: PropTypes.func,
};
export default ExtraAttributesDialog;
export default ExtraMetadataAttributesDialog;

View File

@@ -3,12 +3,11 @@ import PropTypes from 'prop-types';
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import Icon from '../icon';
import { gettext, canSetExProps } from '../../utils/constants';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import EditFileTagPopover from '../popover/edit-filetag-popover';
import ExtraAttributesDialog from '../dialog/extra-attributes-dialog';
import FileTagList from '../file-tag-list';
import ConfirmApplyFolderPropertiesDialog from '../dialog/confirm-apply-folder-properties-dialog';
import ExtraMetadataAttributesDialog from '../dialog/extra-metadata-attributes-dialog';
const propTypes = {
repoInfo: PropTypes.object.isRequired,
@@ -28,8 +27,7 @@ class DetailListView extends React.Component {
super(props);
this.state = {
isEditFileTagShow: false,
isShowExtraProperties: false,
isShowApplyProperties: false
isShowMetadataExtraProperties: false,
};
this.tagListTitleID = `detail-list-view-tags-${uuidv4()}`;
}
@@ -65,12 +63,8 @@ class DetailListView extends React.Component {
return Utils.joinPath(path, dirent.name);
};
toggleExtraPropertiesDialog = () => {
this.setState({ isShowExtraProperties: !this.state.isShowExtraProperties });
};
toggleApplyPropertiesDialog = () => {
this.setState({ isShowApplyProperties: !this.state.isShowApplyProperties });
toggleExtraMetadataPropertiesDialog = () => {
this.setState({ isShowMetadataExtraProperties: !this.state.isShowMetadataExtraProperties });
};
renderTags = () => {
@@ -85,23 +79,12 @@ class DetailListView extends React.Component {
<tbody>
<tr><th>{gettext('Location')}</th><td>{position}</td></tr>
<tr><th>{gettext('Last Update')}</th><td>{moment(direntDetail.mtime).format('YYYY-MM-DD')}</td></tr>
{direntDetail.permission === 'rw' && canSetExProps && (
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<Fragment>
<tr className="file-extra-attributes">
<th colSpan={2}>
<div className="edit-file-extra-attributes-btn" onClick={this.toggleExtraPropertiesDialog}>
{gettext('Edit extra properties')}
</div>
</th>
</tr>
<tr className="file-extra-attributes">
<th colSpan={2}>
<div
className="edit-file-extra-attributes-btn text-truncate"
onClick={this.toggleApplyPropertiesDialog}
title={gettext('Apply properties to files inside the folder')}
>
{gettext('Apply properties to files inside the folder')}
<div className="edit-file-extra-attributes-btn" onClick={this.toggleExtraMetadataPropertiesDialog}>
{gettext('Edit metadata properties')}
</div>
</th>
</tr>
@@ -127,11 +110,11 @@ class DetailListView extends React.Component {
<span onClick={this.onEditFileTagToggle} id={this.tagListTitleID}><Icon symbol='tag' /></span>
</td>
</tr>
{direntDetail.permission === 'rw' && canSetExProps && (
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<tr className="file-extra-attributes">
<th colSpan={2}>
<div className="edit-file-extra-attributes-btn" onClick={this.toggleExtraPropertiesDialog}>
{gettext('Edit extra properties')}
<div className="edit-file-extra-attributes-btn" onClick={this.toggleExtraMetadataPropertiesDialog}>
{gettext('Edit metadata properties')}
</div>
</th>
</tr>
@@ -160,20 +143,13 @@ class DetailListView extends React.Component {
isEditFileTagShow={this.state.isEditFileTagShow}
/>
}
{this.state.isShowExtraProperties && (
<ExtraAttributesDialog
{this.state.isShowMetadataExtraProperties && (
<ExtraMetadataAttributesDialog
repoID={this.props.repoID}
filePath={direntPath}
direntType={direntType}
direntDetail={direntDetail}
onToggle={this.toggleExtraPropertiesDialog}
/>
)}
{this.state.isShowApplyProperties && (
<ConfirmApplyFolderPropertiesDialog
toggle={this.toggleApplyPropertiesDialog}
repoID={this.props.repoID}
path={direntPath}
onToggle={this.toggleExtraMetadataPropertiesDialog}
/>
)}
</Fragment>

View File

@@ -77,24 +77,35 @@ class MetadataManagerAPI {
return this.req.post(url, data);
}
updateMetadataRecord = (repoID, recordID, creator, createTime, modifier, modifyTime, parentDir, name) => {
addMetadataColumn(repoID, column_name) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/columns/';
let data = {
'column_name': column_name
};
return this.req.post(url, data);
}
getMetadataRecordInfo(repoID, parentDir, name) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/record/';
let params = {};
if (parentDir) {
params['parent_dir'] = parentDir;
}
if (name) {
params['name'] = name;
}
return this.req.get(url, {params: params});
}
updateMetadataRecord = (repoID, recordID, columnName, newValue) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/records/' + recordID + '/';
const data = {
'creator': creator,
'create_time': createTime,
'modifier': modifier,
'modify_time': modifyTime,
'current_dir': parentDir,
'name': name,
'column_name': columnName,
'value': newValue,
};
return this.req.put(url, data);
};
deleteMetadataRecord = (repoID, recordID) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/records/' + recordID + '/';
return this.req.delete(url);
};
listUserInfo = (userIds) => {
const url = this.server + '/api/v2.1/user-list/';
const params = { user_id_list: userIds };

View File

@@ -97,8 +97,6 @@ export const enablePDFThumbnail = window.app.pageOptions.enablePDFThumbnail;
export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false;
export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || [];
export const canSetExProps = window.app.pageOptions.canSetExProps || false;
// seafile_ai
export const enableSeafileAI = window.app.pageOptions.enableSeafileAI || false;

View File

@@ -1,64 +0,0 @@
import logging
import requests
LEDGER_COLUMNS = [
{'column_key': '0000', 'column_name': 'Repo ID', 'column_type': 'text', 'column_data': None},
{'column_key': 'GqGh', 'column_name': 'File', 'column_type': 'text', 'column_data': {'enable_fill_default_value': False, 'enable_check_format': False, 'format_specification_value': None, 'default_value': '', 'format_check_type': 'chinese_id_card'}},
{'column_key': 'l76s', 'column_name': 'UUID', 'column_type': 'text', 'column_data': {'enable_fill_default_value': False, 'enable_check_format': False, 'format_specification_value': None, 'default_value': '', 'format_check_type': 'chinese_id_card'}},
{'column_key': '1fUd', 'column_name': 'Path', 'column_type': 'text', 'column_data': {'enable_fill_default_value': False, 'enable_check_format': False, 'format_specification_value': None, 'default_value': '', 'format_check_type': 'chinese_id_card'}},
{'column_key': 'IFzK', 'column_name': '文件大分类', 'column_type': 'single-select', 'column_data': {'enable_fill_default_value': False, 'default_value': None, 'options': []}},
{'column_key': 'qc3L', 'column_name': '文件中分类', 'column_type': 'single-select', 'column_data': {'enable_fill_default_value': False, 'default_value': None, 'options': []}},
{'column_key': 'k93T', 'column_name': '文件小分类', 'column_type': 'single-select', 'column_data': {'enable_fill_default_value': False, 'default_value': None, 'options': []}},
{'column_key': 'sysV', 'column_name': '文件负责人', 'column_type': 'text', 'column_data': {'enable_fill_default_value': False, 'enable_check_format': False, 'format_specification_value': None, 'default_value': '', 'format_check_type': 'chinese_id_card'}},
{'column_key': 'TZw3', 'column_name': '密级', 'column_type': 'single-select', 'column_data': {'enable_fill_default_value': False, 'default_value': None, 'options': []}},
{'column_key': 'uFNa', 'column_name': '保密期限', 'column_type': 'number', 'column_data': {'format': 'number', 'precision': 2, 'enable_precision': False, 'enable_fill_default_value': False, 'enable_check_format': False, 'decimal': 'dot', 'thousands': 'no', 'format_min_value': 0, 'format_max_value': 1000}},
{'column_key': 'BeVA', 'column_name': '创建日期', 'column_type': 'date', 'column_data': {'format': 'YYYY-MM-DD HH:mm', 'enable_fill_default_value': False, 'default_value': '', 'default_date_type': 'specific_date'}},
{'column_key': 'ngbE', 'column_name': '废弃日期', 'column_type': 'formula', 'column_data': {'format': 'YYYY-MM-DD', 'formula': "dateAdd({创建日期}, {保密期限}, 'days')", 'operated_columns': ['BeVA', 'uFNa'], 'result_type': 'date'}}
]
DTABLE_WEB_SERVER = ''
SEATABLE_EXTENDED_PROPS_BASE_API_TOKEN = ''
EXTENDED_PROPS_TABLE_NAME = ''
# auth
url = f"{DTABLE_WEB_SERVER.strip('/')}/api/v2.1/dtable/app-access-token/?from=dtable_web"
resp = requests.get(url, headers={'Authorization': f'Token {SEATABLE_EXTENDED_PROPS_BASE_API_TOKEN}'})
dtable_uuid = resp.json()['dtable_uuid']
access_token = resp.json()['access_token']
dtable_server_url = resp.json()['dtable_server']
headers = {'Authorization': f'Token {access_token}'}
# query metadata
url = f"{dtable_server_url.strip('/')}/api/v1/dtables/{dtable_uuid}/metadata/?from=dtable_web"
resp = requests.get(url, headers=headers)
metadata = resp.json()['metadata']
existed_table = None
for table in metadata['tables']:
if table['name'] == EXTENDED_PROPS_TABLE_NAME:
existed_table = table
break
# check table or add table
if existed_table:
logging.info('table %s exists', EXTENDED_PROPS_TABLE_NAME)
for col in LEDGER_COLUMNS:
target_col = None
for table_col in existed_table['columns']:
if col['column_name'] == table_col['name']:
target_col = table_col
break
if not target_col:
logging.error('Column %s not found', col['column_name'])
exit(1)
if target_col['type'] != col['column_type']:
logging.error('Column %s type should be %s', col['column_name'], col['column_type'])
exit(1)
else:
# add table
url = f"{dtable_server_url.strip('/')}/api/v1/dtables/{dtable_uuid}/tables/?from=dtable_web"
data = {
'table_name': EXTENDED_PROPS_TABLE_NAME,
'columns': LEDGER_COLUMNS
}
resp = requests.post(url, headers=headers, json=data)

View File

@@ -1,539 +0,0 @@
import hashlib
import json
import logging
import os
import stat
from collections import defaultdict
from datetime import datetime
from threading import Lock
from uuid import uuid4
import requests
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from seaserv import seafile_api
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.settings import DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, \
EX_PROPS_TABLE, EX_EDITABLE_COLUMNS
from seahub.tags.models import FileUUIDMap
from seahub.utils import normalize_file_path, EMPTY_SHA1
from seahub.utils.repo import parse_repo_perm
from seahub.utils.seatable_api import SeaTableAPI
from seahub.views import check_folder_permission
logger = logging.getLogger(__name__)
class QueryException(Exception):
pass
def check_table(seatable_api: SeaTableAPI):
"""check EX_PROPS_TABLE is invalid or not
:return: error_msg -> str or None
"""
table = seatable_api.get_table_by_name(EX_PROPS_TABLE)
if not table:
return 'Table %s not found' % EX_PROPS_TABLE
return None
class ExtendedPropertiesView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request, repo_id):
if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)):
return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled')
# arguments check
path = request.data.get('path')
if not path:
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid')
path = normalize_file_path(path)
parent_dir = os.path.dirname(path)
dirent_name = os.path.basename(path)
props_data_str = request.data.get('props_data')
if not props_data_str or not isinstance(props_data_str, str):
return api_error(status.HTTP_400_BAD_REQUEST, 'props_data invalid')
try:
props_data = json.loads(props_data_str)
except:
return api_error(status.HTTP_400_BAD_REQUEST, 'props_data invalid')
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library not found')
dirent = seafile_api.get_dirent_by_path(repo_id, path)
if not dirent:
return api_error(status.HTTP_404_NOT_FOUND, 'File or folder %s not found' % path)
if not stat.S_ISDIR(dirent.mode) and dirent.obj_id == EMPTY_SHA1:
return api_error(status.HTTP_400_BAD_REQUEST, 'File or folder %s is empty' % path)
# permission check
if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web:
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# check base
try:
seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER)
except:
logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN)
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid')
## props table
try:
error_msg = check_table(seatable_api)
except Exception as e:
logger.exception('check ex-props table %s error: %s', EX_PROPS_TABLE, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if error_msg:
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid: %s' % error_msg)
## check existed props row
file_uuid = None
if not stat.S_ISDIR(dirent.mode):
file_uuid = FileUUIDMap.objects.get_or_create_fileuuidmap(repo_id, parent_dir, dirent_name, False).uuid.hex
sql = f"SELECT COUNT(1) as `count` FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
result = seatable_api.query(sql)
count = result['results'][0]['count']
if count > 0:
return api_error(status.HTTP_400_BAD_REQUEST, 'The props of the file exists')
## append props row
props_data = {column_name: value for column_name, value in props_data.items() if column_name in EX_EDITABLE_COLUMNS}
props_data.update({
'Repo ID': repo_id,
'File': dirent_name,
'Path': path,
'UUID': file_uuid,
'创建日期': str(datetime.fromtimestamp(dirent.mtime)),
'文件负责人': email2nickname(request.user.username)
})
try:
seatable_api.append_row(EX_PROPS_TABLE, props_data)
except Exception as e:
logger.error('update props table error: %s', e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
## query
sql = f"SELECT * FROM {EX_PROPS_TABLE} WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
try:
result = seatable_api.query(sql)
except Exception as e:
logger.exception('query sql: %s error: %s', sql, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
rows = result.get('results')
row = rows[0] if rows else {}
return Response({
'row': row,
'metadata': result['metadata'],
'editable_columns': EX_EDITABLE_COLUMNS
})
def get(self, request, repo_id):
if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)):
return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled')
# arguments check
path = request.GET.get('path')
if not path:
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid')
path = normalize_file_path(path)
parent_dir = os.path.dirname(path)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library not found')
dirent = seafile_api.get_dirent_by_path(repo_id, path)
if not dirent:
return api_error(status.HTTP_404_NOT_FOUND, 'File or folder %s not found' % path)
# permission check
if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web:
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# check base
try:
seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER)
except:
logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN)
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid')
## props table
try:
error_msg = check_table(seatable_api)
except Exception as e:
logger.exception('check ex-props table %s error: %s', EX_PROPS_TABLE, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if error_msg:
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid: %s' % error_msg)
## query
dirent_name = os.path.basename(path)
file_uuid = None
if not stat.S_ISDIR(dirent.mode):
file_uuid = FileUUIDMap.objects.get_or_create_fileuuidmap(repo_id, parent_dir, dirent_name, False).uuid.hex
sql = f"SELECT * FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
try:
result = seatable_api.query(sql)
except Exception as e:
logger.exception('query sql: %s error: %s', sql, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
rows = result.get('results')
if rows:
row = rows[0]
else:
row = {
'Repo ID': repo_id,
'File': dirent_name,
'Path': path,
'UUID': file_uuid
}
for name in ['Repo ID', 'File', 'Path', 'UUID']:
for column in result['metadata']:
if name == column['name']:
row[column['key']] = row[name]
row.pop(name, None)
break
return Response({
'row': row,
'metadata': result['metadata'],
'editable_columns': EX_EDITABLE_COLUMNS
})
def put(self, request, repo_id):
if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)):
return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled')
# arguments check
path = request.data.get('path')
if not path:
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid')
path = normalize_file_path(path)
parent_dir = os.path.dirname(path)
props_data_str = request.data.get('props_data')
if not props_data_str or not isinstance(props_data_str, str):
return api_error(status.HTTP_400_BAD_REQUEST, 'props_data invalid')
try:
props_data = json.loads(props_data_str)
except:
return api_error(status.HTTP_400_BAD_REQUEST, 'props_data invalid')
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library not found')
dirent = seafile_api.get_dirent_by_path(repo_id, path)
if not dirent:
return api_error(status.HTTP_404_NOT_FOUND, 'File or folder %s not found' % path)
# permission check
if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web:
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# check base
try:
seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER)
except:
logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN)
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid')
## props table
try:
error_msg = check_table(seatable_api)
except Exception as e:
logger.exception('check ex-props table %s error: %s', EX_PROPS_TABLE, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if error_msg:
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid: %s' % error_msg)
## check existed props row
sql = f"SELECT * FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
result = seatable_api.query(sql)
results = result['results']
if not results:
return api_error(status.HTTP_404_NOT_FOUND, 'The props of the file or folder not found')
row_id = results[0]['_id']
## update props row
props_data = {col_name: value for col_name, value in props_data.items() if col_name in EX_EDITABLE_COLUMNS}
try:
seatable_api.update_row(EX_PROPS_TABLE, row_id, props_data)
except Exception as e:
logger.error('update props table error: %s', e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
## query
sql = f"SELECT * FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
try:
result = seatable_api.query(sql)
except Exception as e:
logger.exception('query sql: %s error: %s', sql, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
rows = result.get('results')
row = rows[0] if rows else {}
return Response({
'row': row,
'metadata': result['metadata'],
'editable_columns': EX_EDITABLE_COLUMNS
})
def delete(self, request, repo_id):
if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)):
return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled')
# arguments check
path = request.GET.get('path')
if not path:
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid')
path = normalize_file_path(path)
parent_dir = os.path.dirname(path)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library not found')
dirent = seafile_api.get_dirent_by_path(repo_id, path)
if not dirent:
return api_error(status.HTTP_404_NOT_FOUND, 'File or folder %s not found' % path)
# permission check
if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web:
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# check base
try:
seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER)
except:
logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN)
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid')
## props table
try:
error_msg = check_table(seatable_api)
except Exception as e:
logger.exception('check ex-props table %s error: %s', EX_PROPS_TABLE, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if error_msg:
return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid: %s' % error_msg)
sql = f"DELETE FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'"
try:
seatable_api.query(sql)
except Exception as e:
logger.exception('delete props record error: %s', e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'success': True})
class ApplyFolderExtendedPropertiesView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
list_max = 1000
step = 500
def md5_repo_id_parent_path(self, repo_id, parent_path):
parent_path = parent_path.rstrip('/') if parent_path != '/' else '/'
return hashlib.md5((repo_id + parent_path).encode('utf-8')).hexdigest()
def query_fileuuids_map(self, repo_id, file_paths):
"""
:return: {file_path: fileuuid}
"""
file_path_2_uuid_map = {}
no_uuid_file_paths = []
# query uuids
for i in range(0, len(file_paths), self.step):
parent_path_2_filenames_map = defaultdict(list)
for file_path in file_paths[i: i+self.step]:
parent_path, filename = os.path.split(file_path)
parent_path_2_filenames_map[parent_path].append(filename)
for parent_path, filenames in parent_path_2_filenames_map.items():
md5 = self.md5_repo_id_parent_path(repo_id, parent_path)
results = FileUUIDMap.objects.filter(repo_id=repo_id, repo_id_parent_path_md5=md5, filename__in=filenames)
for uuid_item in results:
file_path_2_uuid_map[os.path.join(parent_path, uuid_item.filename)] = uuid_item.uuid.hex
## some filename no uuids
for filename in filenames:
cur_file_path = os.path.join(parent_path, filename)
if cur_file_path not in file_path_2_uuid_map:
no_uuid_file_paths.append({'file_path': cur_file_path, 'uuid': uuid4().hex, 'repo_id_parent_path_md5': md5})
# create uuids
for i in range(0, len(no_uuid_file_paths), self.step):
uuid_objs = []
for j in range(i, min(i+self.step, len(no_uuid_file_paths))):
no_uuid_file_path = no_uuid_file_paths[j]
kwargs = {
'uuid': no_uuid_file_path['uuid'],
'repo_id': repo_id,
'repo_id_parent_path_md5': no_uuid_file_path['repo_id_parent_path_md5'],
'parent_path': os.path.dirname(no_uuid_file_path['file_path']),
'filename': os.path.basename(no_uuid_file_path['file_path']),
'is_dir': 0
}
uuid_objs.append(FileUUIDMap(**kwargs))
FileUUIDMap.objects.bulk_create(uuid_objs)
for j in range(i, min(i+self.step, len(no_uuid_file_paths))):
file_path_2_uuid_map[no_uuid_file_paths[j]['file_path']] = no_uuid_file_paths[j]['uuid']
return file_path_2_uuid_map
def query_path_2_row_id_map(self, repo_id, query_list, seatable_api: SeaTableAPI):
"""
:return: path_2_row_id_map -> {path: row_id}
"""
path_2_row_id_map = {}
for i in range(0, len(query_list), self.step):
paths_str = ', '.join(map(lambda x: f"'{x['path']}'", query_list[i: i+self.step]))
sql = f"SELECT `_id`, `Path` FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path` IN ({paths_str})"
resp_json = seatable_api.query(sql, convert=True)
rows = resp_json['results']
path_2_row_id_map.update({row['Path']: row['_id'] for row in rows})
return path_2_row_id_map
def query_ex_props_by_path(self, repo_id, path, seatable_api: SeaTableAPI):
columns_str = ', '.join(map(lambda x: f"`{x}`", EX_EDITABLE_COLUMNS))
sql = f"SELECT {columns_str} FROM `{EX_PROPS_TABLE}` WHERE `Repo ID` = '{repo_id}' AND `Path` = '{path}'"
resp_json = seatable_api.query(sql, convert=True)
if not resp_json['results']:
return None
row = resp_json['results'][0]
return row
def update_ex_props(self, update_list, ex_props, seatable_api: SeaTableAPI):
for i in range(0, len(update_list), self.step):
updates = []
for j in range(i, min(len(update_list), i+self.step)):
updates.append({
'row_id': update_list[j]['row_id'],
'row': ex_props
})
seatable_api.update_rows_by_dtable_db(EX_PROPS_TABLE, updates)
def insert_ex_props(self, repo_id, insert_list, ex_props, context, seatable_api: SeaTableAPI):
for i in range(0, len(insert_list), self.step):
rows = []
for j in range(i, min(len(insert_list), i+self.step)):
row = {
'Repo ID': repo_id,
'File': os.path.basename(insert_list[j]['path']),
'UUID': insert_list[j].get('fileuuid'),
'Path': insert_list[j]['path'],
'创建日期': str(datetime.fromtimestamp(insert_list[j]['mtime'])),
'文件负责人': context['文件负责人']
}
row.update(ex_props)
rows.append(row)
seatable_api.batch_append_rows(EX_PROPS_TABLE, rows)
def apply_folder(self, repo_id, folder_path, context, seatable_api: SeaTableAPI, folder_props):
stack = [folder_path]
query_list = [] # [{path, type}]
file_query_list = [] # [path]
update_list = [] # [{}]
insert_list = [] # [{}]
# query folder props
while stack:
current_path = stack.pop()
dirents = seafile_api.list_dir_by_path(repo_id, current_path)
if not dirents:
continue
for dirent in dirents:
dirent_path = os.path.join(current_path, dirent.obj_name)
if stat.S_ISDIR(dirent.mode):
query_list.append({'path': dirent_path, 'type': 'dir', 'mtime': dirent.mtime})
stack.append(dirent_path)
else:
if dirent.obj_id == EMPTY_SHA1:
continue
query_list.append({'path': dirent_path, 'type': 'file', 'mtime': dirent.mtime})
file_query_list.append(dirent_path)
# query ex-props
if len(query_list) >= self.list_max:
file_path_2_uuid_map = self.query_fileuuids_map(repo_id, file_query_list)
path_2_row_id_map = self.query_path_2_row_id_map(repo_id, query_list, seatable_api)
for query_item in query_list:
if query_item['path'] in path_2_row_id_map:
query_item['row_id'] = path_2_row_id_map.get(query_item['path'])
update_list.append(query_item)
else:
if query_item['type'] == 'file':
query_item['fileuuid'] = file_path_2_uuid_map.get(query_item['path'])
insert_list.append(query_item)
query_list = file_query_list = []
# update ex-props
if len(update_list) >= self.list_max:
self.update_ex_props(update_list, folder_props, seatable_api)
update_list = []
# insert ex-props
if len(insert_list) >= self.list_max:
self.insert_ex_props(repo_id, insert_list, folder_props, context, seatable_api)
insert_list = []
# handle query/update/insert left
file_path_2_uuid_map = self.query_fileuuids_map(repo_id, file_query_list)
path_2_row_id_map = self.query_path_2_row_id_map(repo_id, query_list, seatable_api)
for query_item in query_list:
if query_item['path'] in path_2_row_id_map:
query_item['row_id'] = path_2_row_id_map.get(query_item['path'])
update_list.append(query_item)
else:
if query_item['type'] == 'file':
query_item['fileuuid'] = file_path_2_uuid_map.get(query_item['path'])
insert_list.append(query_item)
self.update_ex_props(update_list, folder_props, seatable_api)
self.insert_ex_props(repo_id, insert_list, folder_props, context, seatable_api)
def post(self, request, repo_id):
if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)):
return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled')
# arguments check
path = request.data.get('path')
path = normalize_file_path(path)
if not path:
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid')
parent_dir = os.path.dirname(path)
dirent = seafile_api.get_dirent_by_path(repo_id, path)
if not dirent:
return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found' % path)
if not stat.S_ISDIR(dirent.mode):
return api_error(status.HTTP_400_BAD_REQUEST, '%s is not a folder' % path)
# permission check
if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web:
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# request props from seatable
try:
seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER)
except:
logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN)
return api_error(status.HTTP_400_BAD_REQUEST, 'Props base invalid')
try:
folder_props = self.query_ex_props_by_path(repo_id, path, seatable_api)
if not folder_props:
return api_error(status.HTTP_400_BAD_REQUEST, 'The folder is not be set extended properties')
except Exception as e:
logger.exception('query repo: %s folder: %s ex-props error: %s', repo_id, path, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
# apply props
context = {'文件负责人': email2nickname(request.user.username)}
try:
self.apply_folder(repo_id, path, context, seatable_api, folder_props)
except QueryException as e:
logger.exception('apply folder: %s ex-props query dtable-db error: %s', path, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
except Exception as e:
logger.exception('apply folder: %s ex-props error: %s', path, e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'success': True})

View File

@@ -9,7 +9,7 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.repo_metadata.models import RepoMetadata
from seahub.views import check_folder_permission
from seahub.repo_metadata.utils import add_init_metadata_task
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_records
from seaserv import seafile_api
@@ -132,7 +132,7 @@ class MetadataManage(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True})
class MetadataRecords(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
@@ -210,3 +210,207 @@ class MetadataRecords(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(results)
class MetadataRecordInfo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request, repo_id):
parent_dir = request.GET.get('parent_dir')
name = request.GET.get('name')
if not parent_dir:
error_msg = 'parent_dir invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not name:
error_msg = 'name invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not record or not record.enabled:
error_msg = f'The metadata module is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
permission = check_folder_permission(request, repo_id, '/')
if permission != 'rw':
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.metadata_server_api import METADATA_TABLE
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE \
`{METADATA_TABLE.columns.parent_dir.name}`=? AND `{METADATA_TABLE.columns.file_name.name}`=?;'
parameters = [parent_dir, name]
try:
query_result = metadata_server_api.query_rows(sql, parameters)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
sys_columns = [
METADATA_TABLE.columns.id.key,
METADATA_TABLE.columns.file_creator.key,
METADATA_TABLE.columns.file_ctime.key,
METADATA_TABLE.columns.file_modifier.key,
METADATA_TABLE.columns.file_mtime.key,
METADATA_TABLE.columns.parent_dir.key,
METADATA_TABLE.columns.file_name.key,
METADATA_TABLE.columns.is_dir.key,
]
rows = query_result.get('results')
if not rows:
error_msg = 'Record not found'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
metadata = query_result.get('metadata')
editable_columns = []
name_to_key = {}
for col in metadata:
col_key = col.get('key')
col_name = col.get('name')
name_to_key[col_name] = col_key
if col_key in sys_columns:
continue
editable_columns.append(col.get('name'))
row = {name_to_key[name]: value for name, value in rows[0].items()}
query_result['row'] = row
query_result['editable_columns'] = editable_columns
query_result.pop('results', None)
return Response(query_result)
class MetadataRecord(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def put(self, request, repo_id, record_id):
column_name = request.data.get('column_name')
new_value = request.data.get('value')
if not column_name:
error_msg = 'column_name invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not new_value:
error_msg = 'value invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not record or not record.enabled:
error_msg = f'The metadata module is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
permission = check_folder_permission(request, repo_id, '/')
if permission != 'rw':
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.metadata_server_api import METADATA_TABLE
sys_column_names = [
METADATA_TABLE.columns.id.name,
METADATA_TABLE.columns.file_creator.name,
METADATA_TABLE.columns.file_ctime.name,
METADATA_TABLE.columns.file_modifier.name,
METADATA_TABLE.columns.file_mtime.name,
METADATA_TABLE.columns.parent_dir.name,
METADATA_TABLE.columns.file_name.name,
METADATA_TABLE.columns.is_dir.name,
]
if column_name in sys_column_names:
error_msg = 'column_name invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
row = {
METADATA_TABLE.columns.id.name: record_id,
column_name: new_value
}
try:
metadata_server_api.update_rows(METADATA_TABLE.id, [row])
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({'success': True})
class MetadataColumns(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request, repo_id):
column_name = request.data.get('column_name')
column_type = request.data.get('column_type', 'text')
if not column_name:
error_msg = 'column_name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not record or not record.enabled:
error_msg = f'The metadata module is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
permission = check_folder_permission(request, repo_id, '/')
if permission != 'rw':
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.metadata_server_api import METADATA_TABLE, MetadataColumn
columns = metadata_server_api.list_columns(METADATA_TABLE.id).get('columns')
column_keys = set()
column_names = set()
for column in columns:
column_keys.add(column.get('key'))
column_names.add(column.get('name'))
if column_name in column_names:
error_msg = 'column_name duplicated.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
column_key = gen_unique_id(column_keys)
column = MetadataColumn(column_key, column_name, column_type)
try:
metadata_server_api.add_column(METADATA_TABLE.id, column.to_dict())
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({'success': True})

View File

@@ -136,3 +136,11 @@ class MetadataServerAPI:
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/query'
response = requests.post(url, json=post_data, headers=self.headers, timeout=self.timeout)
return parse_response(response)
def list_columns(self, table_id):
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
data = {
'table_id': table_id
}
response = requests.get(url, json=data, headers=self.headers, timeout=self.timeout)
return parse_response(response)

View File

@@ -2,6 +2,7 @@ import jwt
import time
import requests
import json
import random
from urllib.parse import urljoin
from seahub.settings import SECRET_KEY, SEAFEVENTS_SERVER_URL
@@ -14,3 +15,18 @@ def add_init_metadata_task(params):
url = urljoin(SEAFEVENTS_SERVER_URL, '/add-init-metadata-task')
resp = requests.get(url, params=params, headers=headers)
return json.loads(resp.content)['task_id']
def generator_base64_code(length=4):
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
ids = random.sample(possible, length)
return ''.join(ids)
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)

View File

@@ -886,13 +886,6 @@ if os.environ.get('SEAFILE_DOCS', None):
LOGO_WIDTH = ''
ENABLE_WIKI = True
#######################
# extended properties #
#######################
SEATABLE_EX_PROPS_BASE_API_TOKEN = ''
EX_PROPS_TABLE = ''
EX_EDITABLE_COLUMNS = []
##############################
# metadata server properties #
##############################

View File

@@ -150,7 +150,6 @@
onlyofficeConverterExtensions: {% if onlyofficeConverterExtensions %} {{onlyofficeConverterExtensions|safe}} {% else %} null {% endif %},
enableSeadoc: {% if enable_seadoc %} true {% else %} false {% endif %},
enableSeafileAI: {% if enable_seafile_ai %} true {% else %} false {% endif %},
canSetExProps: {% if can_set_ex_props %} true {% else %} false {% endif %},
enableSeaTableIntegration: {% if enable_seatable_integration %} true {% else %} false {% endif %},
isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %},

View File

@@ -120,8 +120,6 @@ from seahub.ocm_via_webdav.ocm_api import OCMProviderView
from seahub.api2.endpoints.repo_share_links import RepoShareLinks, RepoShareLink
from seahub.api2.endpoints.repo_upload_links import RepoUploadLinks, RepoUploadLink
from seahub.api2.endpoints.extended_properties import ExtendedPropertiesView, ApplyFolderExtendedPropertiesView
# Admin
from seahub.api2.endpoints.admin.abuse_reports import AdminAbuseReportsView, AdminAbuseReportView
from seahub.api2.endpoints.admin.revision_tag import AdminTaggedItemsView
@@ -206,7 +204,7 @@ from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskSta
from seahub.wiki2.views import wiki_view
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.metadata_manage import MetadataRecords, MetadataManage, MetadataRecord, MetadataColumns, MetadataRecordInfo
from seahub.api2.endpoints.user_list import UserListView
@@ -444,10 +442,6 @@ urlpatterns = [
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/participant/$', FileParticipantView.as_view(), name='api-v2.1-file-participant'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/related-users/$', RepoRelatedUsersView.as_view(), name='api-v2.1-related-user'),
## user:file:extended-props
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/extended-properties/$', ExtendedPropertiesView.as_view(), name='api-v2.1-extended-properties'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/apply-folder-extended-properties/$', ApplyFolderExtendedPropertiesView.as_view(), name='api-v2.1-apply-folder-extended-properties'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/auto-delete/$', RepoAutoDeleteView.as_view(), name='api-v2.1-repo-auto-delete'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/share-links/$', RepoShareLinks.as_view(), name='api-v2.1-repo-share-links'),
@@ -1036,4 +1030,8 @@ if settings.ENABLE_METADATA_MANAGEMENT:
urlpatterns += [
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/$', MetadataManage.as_view(), name='api-v2.1-metadata'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/records/$', MetadataRecords.as_view(), name='api-v2.1-metadata-records'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/records/(?P<record_id>[A-Za-z0-9_-]+)/$', MetadataRecord.as_view(), name='api-v2.1-metadata-record'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/record/$', MetadataRecordInfo.as_view(), name='api-v2.1-metadata-record-info'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/columns/$', MetadataColumns.as_view(), name='api-v2.1-metadata-columns'),
]

View File

@@ -1,146 +0,0 @@
import requests
class ColumnTypes:
COLLABORATOR = 'collaborator'
NUMBER = 'number'
DATE = 'date'
GEOLOCATION = 'geolocation'
CREATOR = 'creator'
LAST_MODIFIER = 'last-modifier'
TEXT = 'text'
IMAGE = 'image'
LONG_TEXT = 'long-text'
CHECKBOX = 'checkbox'
SINGLE_SELECT = 'single-select'
MULTIPLE_SELECT = 'multiple-select'
URL = 'url'
DURATION = 'duration'
FILE = 'file'
EMAIL = 'email'
RATE = 'rate'
FORMULA = 'formula'
LINK_FORMULA = 'link-formula'
AUTO_NUMBER = 'auto-number'
LINK = 'link'
CTIME = 'ctime'
MTIME = 'mtime'
BUTTON = 'button'
DIGITAL_SIGN = 'digital-sign'
def parse_response(response):
if response.status_code >= 400:
raise ConnectionError(response.status_code, response.text)
else:
try:
return response.json()
except:
pass
class SeaTableAPI:
def __init__(self, api_token, server_url):
self.api_token = api_token
self.server_url = server_url
self.dtable_uuid = None
self.access_token = None
self.dtable_server_url = None
self.dtable_db_url = None
self.headers = None
self.auth()
def auth(self):
url = f"{self.server_url.strip('/')}/api/v2.1/dtable/app-access-token/?from=dtable_web"
resp = requests.get(url, headers={'Authorization': f'Token {self.api_token}'})
self.dtable_uuid = resp.json()['dtable_uuid']
self.access_token = resp.json()['access_token']
self.dtable_server_url = resp.json()['dtable_server']
self.dtable_db_url = resp.json()['dtable_db']
self.headers = {'Authorization': f'Token {self.access_token}'}
def get_metadata(self):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/metadata/?from=dtable_web"
resp = requests.get(url, headers=self.headers)
return parse_response(resp)['metadata']
def query(self, sql, convert=None, server_only=None, parameters=None):
url = f"{self.dtable_db_url.strip('/')}/api/v1/query/{self.dtable_uuid}/?from=dtable_web"
data = {'sql': sql}
if convert is not None:
data['convert_keys'] = convert
if server_only is not None:
data['server_only'] = server_only
if parameters:
data['parameters'] = parameters
resp = requests.post(url, json=data, headers=self.headers)
return parse_response(resp)
def add_table(self, table_name, columns=None):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/tables/?from=dtable_web"
data = {'table_name': table_name}
if columns:
data['columns'] = columns
resp = requests.post(url, headers=self.headers, json=data)
return parse_response(resp)
def insert_column(self, table_name, column):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/columns/?from=dtable_web"
data = {'table_name': table_name}
data.update(column)
resp = requests.post(url, headers=self.headers, json=data)
return parse_response(resp)
def append_row(self, table_name, row):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/rows/?from=dtable_web"
data = {
'table_name': table_name,
'row': row
}
resp = requests.post(url, headers=self.headers, json=data)
return parse_response(resp)
def update_row(self, table_name, row_id, row):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/rows/?from=dtable_web"
data = {
'table_name': table_name,
'row': row,
"row_id": row_id
}
resp = requests.put(url, headers=self.headers, json=data)
return parse_response(resp)
def batch_append_rows(self, table_name, rows):
url = f"{self.dtable_server_url.strip('/')}/api/v1/dtables/{self.dtable_uuid}/batch-append-rows/?from=dtable_web"
data = {
'table_name': table_name,
'rows': rows
}
resp = requests.post(url, headers=self.headers, json=data)
return parse_response(resp)
def get_table_by_name(self, table_name):
metadata = self.get_metadata()
for table in metadata['tables']:
if table['name'] == table_name:
return table
return None
def update_rows_by_dtable_db(self, table_name, updates):
url = f"{self.dtable_db_url.strip('/')}/api/v1/update-rows/{self.dtable_uuid}/?from=dtable_web"
data = {
'table_name': table_name,
'updates': updates
}
resp = requests.put(url, headers=self.headers, json=data)
return parse_response(resp)
def insert_rows_by_dtable_db(self, table_name, rows):
url = f"{self.dtable_db_url.strip('/')}/api/v1/insert-rows/{self.dtable_uuid}/?from=dtable_web"
data = {
'table_name': table_name,
'rows': rows
}
resp = requests.post(url, headers=self.headers, json=data)
return parse_response(resp)

View File

@@ -58,7 +58,7 @@ from seahub.settings import AVATAR_FILE_STORAGE, ENABLE_REPO_SNAPSHOT_LABEL, \
UPLOAD_LINK_EXPIRE_DAYS_MIN, UPLOAD_LINK_EXPIRE_DAYS_MAX, UPLOAD_LINK_EXPIRE_DAYS_DEFAULT, \
SEAFILE_COLLAB_SERVER, ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, \
ADDITIONAL_SHARE_DIALOG_NOTE, ADDITIONAL_APP_BOTTOM_LINKS, ADDITIONAL_ABOUT_DIALOG_LINKS, \
DTABLE_WEB_SERVER, EX_PROPS_TABLE, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_EDITABLE_COLUMNS
DTABLE_WEB_SERVER
from seahub.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS
from seahub.ocm_via_webdav.settings import ENABLE_OCM_VIA_WEBDAV
@@ -1114,6 +1114,5 @@ def react_fake_view(request, **kwargs):
'group_import_members_extra_msg': GROUP_IMPORT_MEMBERS_EXTRA_MSG,
'request_from_onlyoffice_desktop_editor': ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT in request.headers.get('user-agent', ''),
'enable_sso_to_thirdpart_website': settings.ENABLE_SSO_TO_THIRDPART_WEBSITE,
'can_set_ex_props': DTABLE_WEB_SERVER and SEATABLE_EX_PROPS_BASE_API_TOKEN and EX_PROPS_TABLE and EX_EDITABLE_COLUMNS,
'enable_metadata_management': settings.ENABLE_METADATA_MANAGEMENT
})