summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints/drive
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-11-12 02:02:25 +0900
commit0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch)
tree40874799472fa07416f17b50a398ac33b7771905 /packages/backend/src/server/api/endpoints/drive
parentupdate deps (diff)
downloadsharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz
sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2
sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip
refactoring
Resolve #7779
Diffstat (limited to 'packages/backend/src/server/api/endpoints/drive')
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files.ts70
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts57
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/check-existence.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/create.ts89
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/delete.ts53
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/find.ts43
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/show.ts84
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts116
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts64
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/create.ts72
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/delete.ts60
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/find.ts43
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/show.ts49
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts123
-rw-r--r--packages/backend/src/server/api/endpoints/drive/stream.ts59
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 });
+});