diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
| commit | 0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch) | |
| tree | 40874799472fa07416f17b50a398ac33b7771905 /packages/backend/src/server/api/endpoints/drive | |
| parent | update deps (diff) | |
| download | sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2 sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip | |
refactoring
Resolve #7779
Diffstat (limited to 'packages/backend/src/server/api/endpoints/drive')
17 files changed, 1107 insertions, 0 deletions
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts new file mode 100644 index 0000000000..95435e1e43 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -0,0 +1,70 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { DriveFiles } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + folderId: { + validator: $.optional.nullable.type(ID), + default: null, + }, + + type: { + validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/) + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + } + }, +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); + + if (ps.folderId) { + query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); + } else { + query.andWhere('file.folderId IS NULL'); + } + + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } + + const files = await query.take(ps.limit!).getMany(); + + return await DriveFiles.packMany(files, { detail: false, self: true }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts new file mode 100644 index 0000000000..eec7d7877e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -0,0 +1,57 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFiles, Notes } from '@/models/index'; + +export const meta = { + tags: ['drive', 'notes'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + fileId: { + validator: $.type(ID), + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Note', + } + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'c118ece3-2e4b-4296-99d1-51756e32d232', + } + } +}; + +export default define(meta, async (ps, user) => { + // Fetch file + const file = await DriveFiles.findOne({ + id: ps.fileId, + userId: user.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + const notes = await Notes.createQueryBuilder('note') + .where(':file = ANY(note.fileIds)', { file: file.id }) + .getMany(); + + return await Notes.packMany(notes, user, { + detail: true + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts new file mode 100644 index 0000000000..2c36078421 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -0,0 +1,31 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + md5: { + validator: $.str, + } + }, + + res: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, +}; + +export default define(meta, async (ps, user) => { + const file = await DriveFiles.findOne({ + md5: ps.md5, + userId: user.id, + }); + + return file != null; +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts new file mode 100644 index 0000000000..2abc104e6c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -0,0 +1,89 @@ +import * as ms from 'ms'; +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import create from '@/services/drive/add-file'; +import define from '../../../define'; +import { apiLogger } from '../../../logger'; +import { ApiError } from '../../../error'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + limit: { + duration: ms('1hour'), + max: 120 + }, + + requireFile: true, + + kind: 'write:drive', + + params: { + folderId: { + validator: $.optional.nullable.type(ID), + default: null, + }, + + name: { + validator: $.optional.nullable.str, + default: null, + }, + + isSensitive: { + validator: $.optional.either($.bool, $.str), + default: false, + transform: (v: any): boolean => v === true || v === 'true', + }, + + force: { + validator: $.optional.either($.bool, $.str), + default: false, + transform: (v: any): boolean => v === true || v === 'true', + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + }, + + errors: { + invalidFileName: { + message: 'Invalid file name.', + code: 'INVALID_FILE_NAME', + id: 'f449b209-0c60-4e51-84d5-29486263bfd4' + } + } +}; + +export default define(meta, async (ps, user, _, file, cleanup) => { + // Get 'name' parameter + let name = ps.name || file.originalname; + if (name !== undefined && name !== null) { + name = name.trim(); + if (name.length === 0) { + name = null; + } else if (name === 'blob') { + name = null; + } else if (!DriveFiles.validateFileName(name)) { + throw new ApiError(meta.errors.invalidFileName); + } + } else { + name = null; + } + + try { + // Create file + const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive); + return await DriveFiles.pack(driveFile, { self: true }); + } catch (e) { + apiLogger.error(e); + throw new ApiError(); + } finally { + cleanup!(); + } +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts new file mode 100644 index 0000000000..038325694d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -0,0 +1,53 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import { deleteFile } from '@/services/drive/delete-file'; +import { publishDriveStream } from '@/services/stream'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + fileId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '908939ec-e52b-4458-b395-1025195cea58' + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '5eb8d909-2540-4970-90b8-dd6f86088121' + }, + } +}; + +export default define(meta, async (ps, user) => { + const file = await DriveFiles.findOne(ps.fileId); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + // Delete + await deleteFile(file); + + // Publish fileDeleted event + publishDriveStream(user.id, 'fileDeleted', file.id); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts new file mode 100644 index 0000000000..5fea7bbbb0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + md5: { + validator: $.str, + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + } + }, +}; + +export default define(meta, async (ps, user) => { + const files = await DriveFiles.find({ + md5: ps.md5, + userId: user.id, + }); + + return await DriveFiles.packMany(files, { self: true }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts new file mode 100644 index 0000000000..dd419f4c04 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + requireCredential: true as const, + + tags: ['drive'], + + kind: 'read:drive', + + params: { + name: { + validator: $.str + }, + + folderId: { + validator: $.optional.nullable.type(ID), + default: null, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + } + }, +}; + +export default define(meta, async (ps, user) => { + const files = await DriveFiles.find({ + name: ps.name, + userId: user.id, + folderId: ps.folderId + }); + + return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true }))); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts new file mode 100644 index 0000000000..a96ebaa123 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -0,0 +1,84 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFile } from '@/models/entities/drive-file'; +import { DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + fileId: { + validator: $.optional.type(ID), + }, + + url: { + validator: $.optional.str, + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '067bc436-2718-4795-b0fb-ecbe43949e31' + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '25b73c73-68b1-41d0-bad1-381cfdf6579f' + }, + + fileIdOrUrlRequired: { + message: 'fileId or url required.', + code: 'INVALID_PARAM', + id: '89674805-722c-440c-8d88-5641830dc3e4' + } + } +}; + +export default define(meta, async (ps, user) => { + let file: DriveFile | undefined; + + if (ps.fileId) { + file = await DriveFiles.findOne(ps.fileId); + } else if (ps.url) { + file = await DriveFiles.findOne({ + where: [{ + url: ps.url + }, { + webpublicUrl: ps.url + }, { + thumbnailUrl: ps.url + }], + }); + } else { + throw new ApiError(meta.errors.fileIdOrUrlRequired); + } + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + return await DriveFiles.pack(file, { + detail: true, + withUser: true, + self: true + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts new file mode 100644 index 0000000000..f277a9c3dc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -0,0 +1,116 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import { publishDriveStream } from '@/services/stream'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFiles, DriveFolders } from '@/models/index'; +import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + fileId: { + validator: $.type(ID), + }, + + folderId: { + validator: $.optional.nullable.type(ID), + default: undefined as any, + }, + + name: { + validator: $.optional.str.pipe(DriveFiles.validateFileName), + default: undefined as any, + }, + + isSensitive: { + validator: $.optional.bool, + default: undefined as any, + }, + + comment: { + validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), + default: undefined as any, + } + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d' + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '01a53b27-82fc-445b-a0c1-b558465a8ed2' + }, + + noSuchFolder: { + message: 'No such folder.', + code: 'NO_SUCH_FOLDER', + id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73' + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile' + } +}; + +export default define(meta, async (ps, user) => { + const file = await DriveFiles.findOne(ps.fileId); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + if (ps.name) file.name = ps.name; + + if (ps.comment !== undefined) file.comment = ps.comment; + + if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; + + if (ps.folderId !== undefined) { + if (ps.folderId === null) { + file.folderId = null; + } else { + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + file.folderId = folder.id; + } + } + + await DriveFiles.update(file.id, { + name: file.name, + comment: file.comment, + folderId: file.folderId, + isSensitive: file.isSensitive + }); + + const fileObj = await DriveFiles.pack(file, { self: true }); + + // Publish fileUpdated event + publishDriveStream(user.id, 'fileUpdated', fileObj); + + return fileObj; +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts new file mode 100644 index 0000000000..9f10a42d24 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -0,0 +1,64 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import * as ms from 'ms'; +import uploadFromUrl from '@/services/drive/upload-from-url'; +import define from '../../../define'; +import { DriveFiles } from '@/models/index'; +import { publishMainStream } from '@/services/stream'; +import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; + +export const meta = { + tags: ['drive'], + + limit: { + duration: ms('1hour'), + max: 60 + }, + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + url: { + // TODO: Validate this url + validator: $.str, + }, + + folderId: { + validator: $.optional.nullable.type(ID), + default: null, + }, + + isSensitive: { + validator: $.optional.bool, + default: false, + }, + + comment: { + validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), + default: null, + }, + + marker: { + validator: $.optional.nullable.str, + default: null, + }, + + force: { + validator: $.optional.bool, + default: false, + } + } +}; + +export default define(meta, async (ps, user) => { + uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => { + DriveFiles.pack(file, { self: true }).then(packedFile => { + publishMainStream(user.id, 'urlUploadFinished', { + marker: ps.marker, + file: packedFile + }); + }); + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts new file mode 100644 index 0000000000..6f16878b13 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -0,0 +1,58 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { DriveFolders } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + folderId: { + validator: $.optional.nullable.type(ID), + default: null, + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFolder', + } + }, +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + .andWhere('folder.userId = :userId', { userId: user.id }); + + if (ps.folderId) { + query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); + } else { + query.andWhere('folder.parentId IS NULL'); + } + + const folders = await query.take(ps.limit!).getMany(); + + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts new file mode 100644 index 0000000000..80f96bd641 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -0,0 +1,72 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import { publishDriveStream } from '@/services/stream'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFolders } from '@/models/index'; +import { genId } from '@/misc/gen-id'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + name: { + validator: $.optional.str.pipe(DriveFolders.validateFolderName), + default: 'Untitled', + }, + + parentId: { + validator: $.optional.nullable.type(ID), + } + }, + + errors: { + noSuchFolder: { + message: 'No such folder.', + code: 'NO_SUCH_FOLDER', + id: '53326628-a00d-40a6-a3cd-8975105c0f95' + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFolder' + } +}; + +export default define(meta, async (ps, user) => { + // If the parent folder is specified + let parent = null; + if (ps.parentId) { + // Fetch parent folder + parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); + + if (parent == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + } + + // Create folder + const folder = await DriveFolders.insert({ + id: genId(), + createdAt: new Date(), + name: ps.name, + parentId: parent !== null ? parent.id : null, + userId: user.id + }).then(x => DriveFolders.findOneOrFail(x.identifiers[0])); + + const folderObj = await DriveFolders.pack(folder); + + // Publish folderCreated event + publishDriveStream(user.id, 'folderCreated', folderObj); + + return folderObj; +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts new file mode 100644 index 0000000000..38b4aef103 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -0,0 +1,60 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { publishDriveStream } from '@/services/stream'; +import { ApiError } from '../../../error'; +import { DriveFolders, DriveFiles } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + folderId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchFolder: { + message: 'No such folder.', + code: 'NO_SUCH_FOLDER', + id: '1069098f-c281-440f-b085-f9932edbe091' + }, + + hasChildFilesOrFolders: { + message: 'This folder has child files or folders.', + code: 'HAS_CHILD_FILES_OR_FOLDERS', + id: 'b0fc8a17-963c-405d-bfbc-859a487295e1' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Get folder + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + const [childFoldersCount, childFilesCount] = await Promise.all([ + DriveFolders.count({ parentId: folder.id }), + DriveFiles.count({ folderId: folder.id }) + ]); + + if (childFoldersCount !== 0 || childFilesCount !== 0) { + throw new ApiError(meta.errors.hasChildFilesOrFolders); + } + + await DriveFolders.delete(folder.id); + + // Publish folderCreated event + publishDriveStream(user.id, 'folderDeleted', folder.id); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts new file mode 100644 index 0000000000..a6c5a49988 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { DriveFolders } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + name: { + validator: $.str + }, + + parentId: { + validator: $.optional.nullable.type(ID), + default: null, + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFolder', + } + }, +}; + +export default define(meta, async (ps, user) => { + const folders = await DriveFolders.find({ + name: ps.name, + userId: user.id, + parentId: ps.parentId + }); + + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts new file mode 100644 index 0000000000..e907a24f05 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -0,0 +1,49 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFolders } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + folderId: { + validator: $.type(ID), + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFolder', + }, + + errors: { + noSuchFolder: { + message: 'No such folder.', + code: 'NO_SUCH_FOLDER', + id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9' + }, + } +}; + +export default define(meta, async (ps, user) => { + // Get folder + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + return await DriveFolders.pack(folder, { + detail: true + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts new file mode 100644 index 0000000000..612252e6df --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -0,0 +1,123 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import { publishDriveStream } from '@/services/stream'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { DriveFolders } from '@/models/index'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'write:drive', + + params: { + folderId: { + validator: $.type(ID), + }, + + name: { + validator: $.optional.str.pipe(DriveFolders.validateFolderName), + }, + + parentId: { + validator: $.optional.nullable.type(ID), + } + }, + + errors: { + noSuchFolder: { + message: 'No such folder.', + code: 'NO_SUCH_FOLDER', + id: 'f7974dac-2c0d-4a27-926e-23583b28e98e' + }, + + noSuchParentFolder: { + message: 'No such parent folder.', + code: 'NO_SUCH_PARENT_FOLDER', + id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1' + }, + + recursiveNesting: { + message: 'It can not be structured like nesting folders recursively.', + code: 'NO_SUCH_PARENT_FOLDER', + id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1' + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFolder' + } +}; + +export default define(meta, async (ps, user) => { + // Fetch folder + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); + + if (folder == null) { + throw new ApiError(meta.errors.noSuchFolder); + } + + if (ps.name) folder.name = ps.name; + + if (ps.parentId !== undefined) { + if (ps.parentId === folder.id) { + throw new ApiError(meta.errors.recursiveNesting); + } else if (ps.parentId === null) { + folder.parentId = null; + } else { + // Get parent folder + const parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); + + if (parent == null) { + throw new ApiError(meta.errors.noSuchParentFolder); + } + + // Check if the circular reference will occur + async function checkCircle(folderId: any): Promise<boolean> { + // Fetch folder + const folder2 = await DriveFolders.findOne({ + id: folderId + }); + + if (folder2!.id === folder!.id) { + return true; + } else if (folder2!.parentId) { + return await checkCircle(folder2!.parentId); + } else { + return false; + } + } + + if (parent.parentId !== null) { + if (await checkCircle(parent.parentId)) { + throw new ApiError(meta.errors.recursiveNesting); + } + } + + folder.parentId = parent.id; + } + } + + // Update + DriveFolders.update(folder.id, { + name: folder.name, + parentId: folder.parentId + }); + + const folderObj = await DriveFolders.pack(folder); + + // Publish folderUpdated event + publishDriveStream(user.id, 'folderUpdated', folderObj); + + return folderObj; +}); diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts new file mode 100644 index 0000000000..141e02f748 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -0,0 +1,59 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../define'; +import { DriveFiles } from '@/models/index'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + tags: ['drive'], + + requireCredential: true as const, + + kind: 'read:drive', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + + type: { + validator: $.optional.str.match(/^[a-zA-Z\/\-*]+$/) + } + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'DriveFile', + } + }, +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); + + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } + + const files = await query.take(ps.limit!).getMany(); + + return await DriveFiles.packMany(files, { detail: false, self: true }); +}); |