9drive 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +389 -0
- package/backend/dist/app.js +113 -0
- package/backend/dist/config/env.js +75 -0
- package/backend/dist/config/prisma.js +30 -0
- package/backend/dist/middleware/api-key.middleware.js +38 -0
- package/backend/dist/middleware/auth.middleware.js +21 -0
- package/backend/dist/middleware/error.middleware.js +7 -0
- package/backend/dist/modules/api-keys/api-key.routes.js +90 -0
- package/backend/dist/modules/audit-logs/audit-log.routes.js +20 -0
- package/backend/dist/modules/auth/auth.routes.js +201 -0
- package/backend/dist/modules/cdn/cdn.routes.js +105 -0
- package/backend/dist/modules/connected-accounts/connected-account.routes.js +313 -0
- package/backend/dist/modules/files/file.routes.js +1013 -0
- package/backend/dist/modules/files/stream-file.js +10 -0
- package/backend/dist/modules/files/stream-google-file.js +79 -0
- package/backend/dist/modules/folders/folder.routes.js +546 -0
- package/backend/dist/modules/google/google.service.js +485 -0
- package/backend/dist/modules/invites/invite.routes.js +127 -0
- package/backend/dist/modules/provider-configs/provider-config.routes.js +53 -0
- package/backend/dist/modules/public/public.routes.js +44 -0
- package/backend/dist/modules/public-api/public-api.routes.js +8 -0
- package/backend/dist/modules/s3/s3.service.js +104 -0
- package/backend/dist/modules/storage/storage.routes.js +108 -0
- package/backend/dist/modules/system/system.routes.js +663 -0
- package/backend/dist/modules/uploads/upload.routes.js +569 -0
- package/backend/dist/scripts/debug-query.js +19 -0
- package/backend/dist/scripts/seed-google-config-if-present.js +49 -0
- package/backend/dist/scripts/seed-google-config.js +40 -0
- package/backend/dist/scripts/test-api-upload.js +37 -0
- package/backend/dist/scripts/test-external-upload.js +181 -0
- package/backend/dist/scripts/test-s3-connection.js +33 -0
- package/backend/dist/server.js +7 -0
- package/backend/dist/utils/audit.js +20 -0
- package/backend/dist/utils/crypto.js +33 -0
- package/backend/dist/utils/jwt.js +15 -0
- package/backend/dist/utils/password.js +14 -0
- package/backend/prisma/generated/sqlite-client/client.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/client.js +5 -0
- package/backend/prisma/generated/sqlite-client/default.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/default.js +5 -0
- package/backend/prisma/generated/sqlite-client/edge.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/edge.js +436 -0
- package/backend/prisma/generated/sqlite-client/index-browser.js +422 -0
- package/backend/prisma/generated/sqlite-client/index.d.ts +35178 -0
- package/backend/prisma/generated/sqlite-client/index.js +457 -0
- package/backend/prisma/generated/sqlite-client/package.json +183 -0
- package/backend/prisma/generated/sqlite-client/query_engine-windows.dll.node +0 -0
- package/backend/prisma/generated/sqlite-client/query_engine_bg.js +2 -0
- package/backend/prisma/generated/sqlite-client/query_engine_bg.wasm +0 -0
- package/backend/prisma/generated/sqlite-client/runtime/edge-esm.js +35 -0
- package/backend/prisma/generated/sqlite-client/runtime/edge.js +35 -0
- package/backend/prisma/generated/sqlite-client/runtime/index-browser.d.ts +370 -0
- package/backend/prisma/generated/sqlite-client/runtime/index-browser.js +17 -0
- package/backend/prisma/generated/sqlite-client/runtime/library.d.ts +3982 -0
- package/backend/prisma/generated/sqlite-client/runtime/library.js +147 -0
- package/backend/prisma/generated/sqlite-client/runtime/react-native.js +84 -0
- package/backend/prisma/generated/sqlite-client/runtime/wasm-compiler-edge.js +85 -0
- package/backend/prisma/generated/sqlite-client/runtime/wasm-engine-edge.js +38 -0
- package/backend/prisma/generated/sqlite-client/schema.prisma +354 -0
- package/backend/prisma/generated/sqlite-client/wasm-edge-light-loader.mjs +5 -0
- package/backend/prisma/generated/sqlite-client/wasm-worker-loader.mjs +5 -0
- package/backend/prisma/generated/sqlite-client/wasm.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/wasm.js +443 -0
- package/backend/prisma/migrations/20260604084325_init/migration.sql +191 -0
- package/backend/prisma/migrations/20260604090020_global_provider_config/migration.sql +8 -0
- package/backend/prisma/migrations/20260604091957_virtual_folders/migration.sql +32 -0
- package/backend/prisma/migrations/20260604095418_nested_folders/migration.sql +8 -0
- package/backend/prisma/migrations/20260604095705_file_shares/migration.sql +22 -0
- package/backend/prisma/migrations/20260604100802_file_preview_tokens/migration.sql +20 -0
- package/backend/prisma/migrations/20260604120000_add_file_share_token/migration.sql +3 -0
- package/backend/prisma/migrations/20260604130000_workspace_invites/migration.sql +17 -0
- package/backend/prisma/migrations/20260604133000_invite_targets/migration.sql +9 -0
- package/backend/prisma/migrations/20260605090000_query_indexes/migration.sql +12 -0
- package/backend/prisma/migrations/20260605100000_folder_icons/migration.sql +1 -0
- package/backend/prisma/migrations/20260605110000_google_auth_handoff/migration.sql +22 -0
- package/backend/prisma/migrations/20260611120000_s3_storage_support/migration.sql +31 -0
- package/backend/prisma/migrations/20260611130000_upload_routing_policy/migration.sql +14 -0
- package/backend/prisma/migrations/20260611140000_api_keys/migration.sql +21 -0
- package/backend/prisma/migrations/20260630030000_upload_session_resumable_fields/migration.sql +9 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +353 -0
- package/backend/prisma/schema.sqlite.prisma +354 -0
- package/backend/public/assets/index-B4ifIM_G.js +135 -0
- package/backend/public/assets/index-DwOPZrmt.css +2 -0
- package/backend/public/assets/ort-wasm-simd-threaded.jsep-D5Jk56-t.wasm +0 -0
- package/backend/public/assets/ort.bundle.min-CpXgll2e.js +2746 -0
- package/backend/public/assets/ort.bundle.min-OfoG_cy9.mjs +2817 -0
- package/backend/public/assets/ort.webgpu.bundle.min-C1vdBqT7.js +2746 -0
- package/backend/public/assets/ort.webgpu.bundle.min-CEayb2S6.mjs +2817 -0
- package/backend/public/assets/pdf.worker.min-Dswkl-cV.mjs +29 -0
- package/backend/public/favicon.ico +74 -0
- package/backend/public/favicon.svg +74 -0
- package/backend/public/icons.svg +24 -0
- package/backend/public/index.html +34 -0
- package/backend/public/manifest.webmanifest +1 -0
- package/backend/public/maskable-icon.svg +22 -0
- package/backend/public/pwa-192x192.svg +22 -0
- package/backend/public/pwa-512x512.svg +22 -0
- package/backend/public/registerSW.js +1 -0
- package/backend/public/sw.js +1 -0
- package/backend/public/workbox-9c191d2f.js +1 -0
- package/bin/9drive.js +320 -0
- package/package.json +60 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publicRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const prisma_js_1 = require("../../config/prisma.js");
|
|
6
|
+
const crypto_js_1 = require("../../utils/crypto.js");
|
|
7
|
+
const stream_file_js_1 = require("../files/stream-file.js");
|
|
8
|
+
exports.publicRouter = (0, express_1.Router)();
|
|
9
|
+
async function findSharedFile(token) {
|
|
10
|
+
const share = await prisma_js_1.prisma.fileShare.findFirst({
|
|
11
|
+
where: { enabled: true, AND: [{ OR: [{ token }, { tokenHash: (0, crypto_js_1.hashToken)(token) }] }, { OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }] }] },
|
|
12
|
+
include: { file: { include: { connectedAccount: true } } },
|
|
13
|
+
});
|
|
14
|
+
if (!share || share.file.status !== 'active')
|
|
15
|
+
throw new Error('Shared file not found');
|
|
16
|
+
return share.file;
|
|
17
|
+
}
|
|
18
|
+
exports.publicRouter.get('/files/:token', async (req, res, next) => {
|
|
19
|
+
try {
|
|
20
|
+
const file = await findSharedFile(String(req.params.token));
|
|
21
|
+
return res.json({ file: { id: file.id, name: file.name, mimeType: file.mimeType, sizeBytes: file.sizeBytes.toString(), createdAt: file.createdAt } });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return next(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
exports.publicRouter.get('/files/:token/download', async (req, res, next) => {
|
|
28
|
+
try {
|
|
29
|
+
const file = await findSharedFile(String(req.params.token));
|
|
30
|
+
return (0, stream_file_js_1.streamProviderFile)(file, req.headers.range, res, { disposition: 'attachment' });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return next(error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
exports.publicRouter.get('/files/:token/preview', async (req, res, next) => {
|
|
37
|
+
try {
|
|
38
|
+
const file = await findSharedFile(String(req.params.token));
|
|
39
|
+
return (0, stream_file_js_1.streamProviderFile)(file, req.headers.range, res, { disposition: 'inline' });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return next(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publicApiRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const api_key_middleware_js_1 = require("../../middleware/api-key.middleware.js");
|
|
6
|
+
const upload_routes_js_1 = require("../uploads/upload.routes.js");
|
|
7
|
+
exports.publicApiRouter = (0, express_1.Router)();
|
|
8
|
+
exports.publicApiRouter.post('/v1/uploads', (0, api_key_middleware_js_1.requireApiKey)('files:upload'), upload_routes_js_1.handleUpload);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createS3Client = createS3Client;
|
|
4
|
+
exports.getS3ConfigForAccount = getS3ConfigForAccount;
|
|
5
|
+
exports.testS3Connection = testS3Connection;
|
|
6
|
+
exports.buildS3ObjectKey = buildS3ObjectKey;
|
|
7
|
+
exports.uploadS3Object = uploadS3Object;
|
|
8
|
+
exports.deleteS3Object = deleteS3Object;
|
|
9
|
+
exports.syncS3Quota = syncS3Quota;
|
|
10
|
+
exports.streamS3File = streamS3File;
|
|
11
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
12
|
+
const lib_storage_1 = require("@aws-sdk/lib-storage");
|
|
13
|
+
const prisma_js_1 = require("../../config/prisma.js");
|
|
14
|
+
const crypto_js_1 = require("../../utils/crypto.js");
|
|
15
|
+
function contentDisposition(type, fileName) {
|
|
16
|
+
return `${type}; filename="${fileName.replaceAll('"', '')}"`;
|
|
17
|
+
}
|
|
18
|
+
function createS3Client(config) {
|
|
19
|
+
return new client_s3_1.S3Client({
|
|
20
|
+
region: config.region,
|
|
21
|
+
endpoint: config.endpoint ?? undefined,
|
|
22
|
+
forcePathStyle: config.forcePathStyle || Boolean(config.endpoint),
|
|
23
|
+
credentials: {
|
|
24
|
+
accessKeyId: (0, crypto_js_1.decryptText)(config.accessKeyIdEncrypted),
|
|
25
|
+
secretAccessKey: (0, crypto_js_1.decryptText)(config.secretAccessKeyEncrypted),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function getS3ConfigForAccount(accountId, userId) {
|
|
30
|
+
return prisma_js_1.prisma.s3StorageConfig.findFirstOrThrow({ where: { connectedAccountId: accountId, status: 'active', ...(userId ? { userId } : {}) } });
|
|
31
|
+
}
|
|
32
|
+
async function testS3Connection(config) {
|
|
33
|
+
const client = createS3Client(config);
|
|
34
|
+
await client.send(new client_s3_1.HeadBucketCommand({ Bucket: config.bucket }));
|
|
35
|
+
}
|
|
36
|
+
function safeFileName(name) {
|
|
37
|
+
return name.replace(/[\\/]+/g, '-').replace(/[\u0000-\u001f\u007f]+/g, '').slice(0, 180) || 'file';
|
|
38
|
+
}
|
|
39
|
+
function buildS3ObjectKey(config, userId, fileId, fileName) {
|
|
40
|
+
return `${config.prefix.replace(/^\/+|\/+$/g, '')}/${userId}/${fileId}/${safeFileName(fileName)}`;
|
|
41
|
+
}
|
|
42
|
+
async function uploadS3Object(config, key, body, mimeType) {
|
|
43
|
+
const client = createS3Client(config);
|
|
44
|
+
await new lib_storage_1.Upload({
|
|
45
|
+
client,
|
|
46
|
+
params: { Bucket: config.bucket, Key: key, Body: body, ContentType: mimeType },
|
|
47
|
+
}).done();
|
|
48
|
+
}
|
|
49
|
+
async function deleteS3Object(file) {
|
|
50
|
+
const config = await getS3ConfigForAccount(file.connectedAccountId);
|
|
51
|
+
const client = createS3Client(config);
|
|
52
|
+
await client.send(new client_s3_1.DeleteObjectCommand({ Bucket: config.bucket, Key: file.providerFileId }));
|
|
53
|
+
}
|
|
54
|
+
async function syncS3Quota(accountId) {
|
|
55
|
+
const config = await getS3ConfigForAccount(accountId);
|
|
56
|
+
const client = createS3Client(config);
|
|
57
|
+
let usedBytes = 0n;
|
|
58
|
+
let continuationToken;
|
|
59
|
+
do {
|
|
60
|
+
const response = await client.send(new client_s3_1.ListObjectsV2Command({ Bucket: config.bucket, ContinuationToken: continuationToken }));
|
|
61
|
+
for (const object of response.Contents ?? [])
|
|
62
|
+
usedBytes += BigInt(object.Size ?? 0);
|
|
63
|
+
continuationToken = response.NextContinuationToken;
|
|
64
|
+
} while (continuationToken);
|
|
65
|
+
return prisma_js_1.prisma.storageAccount.upsert({
|
|
66
|
+
where: { connectedAccountId: accountId },
|
|
67
|
+
create: {
|
|
68
|
+
connectedAccountId: accountId,
|
|
69
|
+
totalBytes: config.quotaBytes,
|
|
70
|
+
usedBytes,
|
|
71
|
+
availableBytes: config.quotaBytes === null ? null : config.quotaBytes - usedBytes,
|
|
72
|
+
lastSyncedAt: new Date(),
|
|
73
|
+
},
|
|
74
|
+
update: {
|
|
75
|
+
totalBytes: config.quotaBytes,
|
|
76
|
+
usedBytes,
|
|
77
|
+
availableBytes: config.quotaBytes === null ? null : config.quotaBytes - usedBytes,
|
|
78
|
+
lastSyncedAt: new Date(),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function streamS3File(file, range, res, options = {}) {
|
|
83
|
+
const config = await getS3ConfigForAccount(file.connectedAccountId);
|
|
84
|
+
const client = createS3Client(config);
|
|
85
|
+
const response = await client.send(new client_s3_1.GetObjectCommand({ Bucket: config.bucket, Key: file.providerFileId, Range: range }));
|
|
86
|
+
res.status(response.ContentRange ? 206 : 200);
|
|
87
|
+
res.setHeader('Content-Type', response.ContentType ?? file.mimeType);
|
|
88
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
89
|
+
if (options.disposition)
|
|
90
|
+
res.setHeader('Content-Disposition', contentDisposition(options.disposition, file.name));
|
|
91
|
+
if (options.headers) {
|
|
92
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
93
|
+
res.setHeader(key, value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (response.ContentLength !== undefined)
|
|
97
|
+
res.setHeader('Content-Length', response.ContentLength.toString());
|
|
98
|
+
if (response.ContentRange)
|
|
99
|
+
res.setHeader('Content-Range', response.ContentRange);
|
|
100
|
+
const body = response.Body;
|
|
101
|
+
if (!body)
|
|
102
|
+
return res.end();
|
|
103
|
+
return body.pipe(res);
|
|
104
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.storageRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const prisma_js_1 = require("../../config/prisma.js");
|
|
7
|
+
const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
|
|
8
|
+
exports.storageRouter = (0, express_1.Router)();
|
|
9
|
+
exports.storageRouter.use(auth_middleware_js_1.requireAuth);
|
|
10
|
+
function bytesToString(value) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return '0';
|
|
13
|
+
return value.toString();
|
|
14
|
+
}
|
|
15
|
+
const routingModes = ['most_available', 'round_robin', 'priority'];
|
|
16
|
+
const routingPolicySchema = zod_1.z.object({ mode: zod_1.z.enum(routingModes), priorityAccountIds: zod_1.z.array(zod_1.z.string().min(1)).max(100).optional() });
|
|
17
|
+
function normalizePriorityAccountIds(value) {
|
|
18
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
19
|
+
}
|
|
20
|
+
async function getOrCreateRoutingPolicy(userId) {
|
|
21
|
+
return prisma_js_1.prisma.uploadRoutingPolicy.upsert({
|
|
22
|
+
where: { userId },
|
|
23
|
+
create: { userId, mode: 'most_available', priorityAccountIds: [] },
|
|
24
|
+
update: {},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.storageRouter.get('/summary', async (req, res, next) => {
|
|
28
|
+
try {
|
|
29
|
+
const accounts = await prisma_js_1.prisma.connectedAccount.findMany({ where: { userId: req.user.id, status: 'connected' }, include: { storageAccount: true } });
|
|
30
|
+
const summary = accounts.reduce((acc, account) => {
|
|
31
|
+
const storage = account.storageAccount;
|
|
32
|
+
acc.totalBytes += storage?.totalBytes ?? 0n;
|
|
33
|
+
acc.usedBytes += storage?.usedBytes ?? 0n;
|
|
34
|
+
acc.availableBytes += storage?.availableBytes ?? 0n;
|
|
35
|
+
return acc;
|
|
36
|
+
}, { totalBytes: 0n, usedBytes: 0n, availableBytes: 0n });
|
|
37
|
+
return res.json({
|
|
38
|
+
totalBytes: summary.totalBytes.toString(),
|
|
39
|
+
usedBytes: summary.usedBytes.toString(),
|
|
40
|
+
availableBytes: summary.availableBytes.toString(),
|
|
41
|
+
accounts: accounts.map((account) => ({
|
|
42
|
+
id: account.id,
|
|
43
|
+
provider: account.provider,
|
|
44
|
+
email: account.email,
|
|
45
|
+
status: account.status,
|
|
46
|
+
totalBytes: account.storageAccount?.totalBytes?.toString() ?? null,
|
|
47
|
+
usedBytes: account.storageAccount?.usedBytes.toString() ?? '0',
|
|
48
|
+
availableBytes: account.storageAccount?.availableBytes?.toString() ?? null,
|
|
49
|
+
lastSyncedAt: account.storageAccount?.lastSyncedAt ?? null,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return next(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
exports.storageRouter.get('/routing-policy', async (req, res, next) => {
|
|
58
|
+
try {
|
|
59
|
+
const policy = await getOrCreateRoutingPolicy(req.user.id);
|
|
60
|
+
return res.json({ policy: { id: policy.id, mode: policy.mode, priorityAccountIds: normalizePriorityAccountIds(policy.priorityAccountIds), roundRobinCursor: policy.roundRobinCursor } });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return next(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
exports.storageRouter.patch('/routing-policy', async (req, res, next) => {
|
|
67
|
+
try {
|
|
68
|
+
const body = routingPolicySchema.parse(req.body);
|
|
69
|
+
const accountIds = [...new Set(body.priorityAccountIds ?? [])];
|
|
70
|
+
const validAccounts = accountIds.length === 0 ? [] : await prisma_js_1.prisma.connectedAccount.findMany({ where: { id: { in: accountIds }, userId: req.user.id, status: 'connected' }, select: { id: true } });
|
|
71
|
+
const validIds = new Set(validAccounts.map((account) => account.id));
|
|
72
|
+
const priorityAccountIds = accountIds.filter((id) => validIds.has(id));
|
|
73
|
+
const policy = await prisma_js_1.prisma.uploadRoutingPolicy.upsert({
|
|
74
|
+
where: { userId: req.user.id },
|
|
75
|
+
create: { userId: req.user.id, mode: body.mode, priorityAccountIds, roundRobinCursor: 0 },
|
|
76
|
+
update: { mode: body.mode, priorityAccountIds, ...(body.mode !== 'round_robin' ? { roundRobinCursor: 0 } : {}) },
|
|
77
|
+
});
|
|
78
|
+
return res.json({ policy: { id: policy.id, mode: policy.mode, priorityAccountIds: normalizePriorityAccountIds(policy.priorityAccountIds), roundRobinCursor: policy.roundRobinCursor } });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return next(error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
exports.storageRouter.get('/breakdown', async (req, res, next) => {
|
|
85
|
+
try {
|
|
86
|
+
const rows = await prisma_js_1.prisma.$queryRaw `
|
|
87
|
+
SELECT
|
|
88
|
+
CASE
|
|
89
|
+
WHEN mime_type LIKE 'image/%' THEN 'photo'
|
|
90
|
+
WHEN mime_type LIKE 'video/%' THEN 'video'
|
|
91
|
+
ELSE 'document'
|
|
92
|
+
END AS kind,
|
|
93
|
+
COALESCE(SUM(size_bytes), 0) AS bytes
|
|
94
|
+
FROM files
|
|
95
|
+
WHERE user_id = ${req.user.id} AND status = 'active'
|
|
96
|
+
GROUP BY kind
|
|
97
|
+
`;
|
|
98
|
+
const breakdown = { photo: '0', video: '0', document: '0' };
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
if (row.kind === 'photo' || row.kind === 'video' || row.kind === 'document')
|
|
101
|
+
breakdown[row.kind] = bytesToString(row.bytes);
|
|
102
|
+
}
|
|
103
|
+
return res.json(breakdown);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return next(error);
|
|
107
|
+
}
|
|
108
|
+
});
|