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,663 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.systemRouter = void 0;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
|
|
12
|
+
const prisma_js_1 = require("../../config/prisma.js");
|
|
13
|
+
const crypto_js_1 = require("../../utils/crypto.js");
|
|
14
|
+
const busboy_1 = __importDefault(require("busboy"));
|
|
15
|
+
exports.systemRouter = (0, express_1.Router)();
|
|
16
|
+
exports.systemRouter.post('/update', auth_middleware_js_1.requireAuth, (req, res, next) => {
|
|
17
|
+
const projectRoot = path_1.default.resolve(process.cwd(), '..');
|
|
18
|
+
const updateScript = path_1.default.join(projectRoot, 'update.sh');
|
|
19
|
+
// Check if git is installed
|
|
20
|
+
(0, child_process_1.exec)('git --version', (gitError) => {
|
|
21
|
+
if (gitError) {
|
|
22
|
+
return res.status(400).json({
|
|
23
|
+
code: 'GIT_NOT_FOUND',
|
|
24
|
+
message: 'Git is not installed inside the app container. Since you are running 9Drive in Docker, please update by running:\n\n1. ssh root@103.65.237.136\n2. cd 9drive\n3. git pull\n4. docker-compose down && docker-compose up -d --build\n\ndirectly in your VPS host terminal.'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (fs_1.default.existsSync(updateScript)) {
|
|
28
|
+
try {
|
|
29
|
+
// Clear old update log to prevent race conditions on frontend polling
|
|
30
|
+
const logFile = path_1.default.join(projectRoot, 'update.log');
|
|
31
|
+
fs_1.default.writeFileSync(logFile, 'Initiating update...\n');
|
|
32
|
+
const child = (0, child_process_1.spawn)('bash', ['update.sh'], {
|
|
33
|
+
cwd: projectRoot,
|
|
34
|
+
detached: true,
|
|
35
|
+
stdio: 'ignore'
|
|
36
|
+
});
|
|
37
|
+
child.unref();
|
|
38
|
+
return res.json({
|
|
39
|
+
status: 'success',
|
|
40
|
+
message: 'System update initiated. Rebuilding and restarting backend & frontend in the background. Please wait ~1 minute and refresh the page.'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return res.status(500).json({
|
|
45
|
+
code: 'UPDATE_FAILED',
|
|
46
|
+
message: 'Failed to start update script.',
|
|
47
|
+
error: err.message
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Fallback to simple git pull if update.sh doesn't exist
|
|
53
|
+
(0, child_process_1.exec)('git pull', { cwd: projectRoot }, (error, stdout, stderr) => {
|
|
54
|
+
if (error) {
|
|
55
|
+
console.error('System update failed:', error);
|
|
56
|
+
return res.status(500).json({
|
|
57
|
+
code: 'UPDATE_FAILED',
|
|
58
|
+
message: 'Failed to run git pull. Make sure git is installed and configured.',
|
|
59
|
+
error: error.message,
|
|
60
|
+
stderr
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
console.log('System update stdout:', stdout);
|
|
64
|
+
if (stderr) {
|
|
65
|
+
console.warn('System update stderr:', stderr);
|
|
66
|
+
}
|
|
67
|
+
return res.json({
|
|
68
|
+
status: 'success',
|
|
69
|
+
message: 'System code updated successfully. Dev servers will auto-restart.',
|
|
70
|
+
stdout,
|
|
71
|
+
stderr
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
exports.systemRouter.get('/update-log', auth_middleware_js_1.requireAuth, (req, res) => {
|
|
78
|
+
const projectRoot = path_1.default.resolve(process.cwd(), '..');
|
|
79
|
+
const logFile = path_1.default.join(projectRoot, 'update.log');
|
|
80
|
+
if (!fs_1.default.existsSync(logFile)) {
|
|
81
|
+
return res.json({
|
|
82
|
+
log: 'No update history found.'
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const logContent = fs_1.default.readFileSync(logFile, 'utf8');
|
|
87
|
+
return res.json({
|
|
88
|
+
log: logContent
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return res.status(500).json({
|
|
93
|
+
code: 'READ_LOG_FAILED',
|
|
94
|
+
message: 'Failed to read update log file.',
|
|
95
|
+
error: error.message
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
exports.systemRouter.get('/google-config', auth_middleware_js_1.requireAuth, async (req, res, next) => {
|
|
100
|
+
try {
|
|
101
|
+
const config = await prisma_js_1.prisma.providerConfig.findFirst({
|
|
102
|
+
where: { userId: null, provider: 'google_drive', status: 'active' },
|
|
103
|
+
orderBy: { createdAt: 'desc' }
|
|
104
|
+
});
|
|
105
|
+
const defaultRedirect = `${req.protocol}://${req.get('host')}/connected-accounts/google/callback`;
|
|
106
|
+
if (!config) {
|
|
107
|
+
return res.json({
|
|
108
|
+
exists: false,
|
|
109
|
+
defaultRedirectUri: defaultRedirect
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
let clientId = '';
|
|
113
|
+
try {
|
|
114
|
+
clientId = (0, crypto_js_1.decryptText)(config.clientIdEncrypted);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
clientId = '';
|
|
118
|
+
}
|
|
119
|
+
return res.json({
|
|
120
|
+
exists: true,
|
|
121
|
+
clientId,
|
|
122
|
+
redirectUri: config.redirectUri,
|
|
123
|
+
hasSecret: !!config.clientSecretEncrypted,
|
|
124
|
+
defaultRedirectUri: defaultRedirect
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return next(error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
exports.systemRouter.post('/google-config', auth_middleware_js_1.requireAuth, async (req, res, next) => {
|
|
132
|
+
try {
|
|
133
|
+
const { clientId, clientSecret, redirectUri } = req.body;
|
|
134
|
+
if (!clientId) {
|
|
135
|
+
return res.status(400).json({ code: 'BAD_REQUEST', message: 'Client ID is required.' });
|
|
136
|
+
}
|
|
137
|
+
const defaultRedirect = `${req.protocol}://${req.get('host')}/connected-accounts/google/callback`;
|
|
138
|
+
const finalRedirectUri = redirectUri || defaultRedirect;
|
|
139
|
+
const scopes = [
|
|
140
|
+
'https://www.googleapis.com/auth/drive',
|
|
141
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
142
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
143
|
+
];
|
|
144
|
+
// Disable old global active config
|
|
145
|
+
await prisma_js_1.prisma.providerConfig.updateMany({
|
|
146
|
+
where: { userId: null, provider: 'google_drive', status: 'active' },
|
|
147
|
+
data: { status: 'disabled' }
|
|
148
|
+
});
|
|
149
|
+
// Retrieve the old config to see if we need to reuse the secret if it was not provided in the request
|
|
150
|
+
let finalSecret = clientSecret;
|
|
151
|
+
if (!finalSecret) {
|
|
152
|
+
const oldConfig = await prisma_js_1.prisma.providerConfig.findFirst({
|
|
153
|
+
where: { userId: null, provider: 'google_drive', status: 'disabled' },
|
|
154
|
+
orderBy: { createdAt: 'desc' }
|
|
155
|
+
});
|
|
156
|
+
if (oldConfig) {
|
|
157
|
+
try {
|
|
158
|
+
finalSecret = (0, crypto_js_1.decryptText)(oldConfig.clientSecretEncrypted);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!finalSecret) {
|
|
166
|
+
return res.status(400).json({ code: 'BAD_REQUEST', message: 'Client Secret is required for first-time setup.' });
|
|
167
|
+
}
|
|
168
|
+
const config = await prisma_js_1.prisma.providerConfig.create({
|
|
169
|
+
data: {
|
|
170
|
+
userId: null,
|
|
171
|
+
provider: 'google_drive',
|
|
172
|
+
clientIdEncrypted: (0, crypto_js_1.encryptText)(clientId),
|
|
173
|
+
clientSecretEncrypted: (0, crypto_js_1.encryptText)(finalSecret),
|
|
174
|
+
redirectUri: finalRedirectUri,
|
|
175
|
+
scopes,
|
|
176
|
+
status: 'active'
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return res.status(201).json({
|
|
180
|
+
status: 'success',
|
|
181
|
+
message: 'Global Google OAuth configuration updated successfully.',
|
|
182
|
+
id: config.id
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return next(error);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
exports.systemRouter.get('/backup', auth_middleware_js_1.requireAuth, async (req, res, next) => {
|
|
190
|
+
try {
|
|
191
|
+
const isSqlite = Boolean(process.env.DATABASE_URL?.startsWith('file:') || process.env.DATABASE_URL?.startsWith('sqlite:'));
|
|
192
|
+
if (isSqlite) {
|
|
193
|
+
const dbPath = getDatabaseFilePath();
|
|
194
|
+
if (fs_1.default.existsSync(dbPath)) {
|
|
195
|
+
res.setHeader('Content-Disposition', 'attachment; filename="9drive-backup.db"');
|
|
196
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
197
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
|
|
198
|
+
const fileStream = fs_1.default.createReadStream(dbPath);
|
|
199
|
+
return fileStream.pipe(res);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Export all tables as structured snapshot
|
|
203
|
+
const [users, apiKeys, uploadRoutingPolicies, userSessions, authHandoffs, providerConfigs, oauthStates, connectedAccounts, s3StorageConfigs, storageAccounts, folders, files, fileShares, filePreviewTokens, uploadSessions, auditLogs, workspaceInvites,] = await Promise.all([
|
|
204
|
+
prisma_js_1.prisma.user.findMany(),
|
|
205
|
+
prisma_js_1.prisma.apiKey.findMany(),
|
|
206
|
+
prisma_js_1.prisma.uploadRoutingPolicy.findMany(),
|
|
207
|
+
prisma_js_1.prisma.userSession.findMany(),
|
|
208
|
+
prisma_js_1.prisma.authHandoff.findMany(),
|
|
209
|
+
prisma_js_1.prisma.providerConfig.findMany(),
|
|
210
|
+
prisma_js_1.prisma.oauthState.findMany(),
|
|
211
|
+
prisma_js_1.prisma.connectedAccount.findMany(),
|
|
212
|
+
prisma_js_1.prisma.s3StorageConfig.findMany(),
|
|
213
|
+
prisma_js_1.prisma.storageAccount.findMany(),
|
|
214
|
+
prisma_js_1.prisma.folder.findMany(),
|
|
215
|
+
prisma_js_1.prisma.file.findMany(),
|
|
216
|
+
prisma_js_1.prisma.fileShare.findMany(),
|
|
217
|
+
prisma_js_1.prisma.filePreviewToken.findMany(),
|
|
218
|
+
prisma_js_1.prisma.uploadSession.findMany(),
|
|
219
|
+
prisma_js_1.prisma.auditLog.findMany(),
|
|
220
|
+
prisma_js_1.prisma.workspaceInvite.findMany(),
|
|
221
|
+
]);
|
|
222
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
223
|
+
const backup = {
|
|
224
|
+
version: '1.0',
|
|
225
|
+
appName: '9Drive',
|
|
226
|
+
exportedAt: new Date().toISOString(),
|
|
227
|
+
provider: isSqlite ? 'sqlite' : 'mysql',
|
|
228
|
+
data: {
|
|
229
|
+
users,
|
|
230
|
+
apiKeys,
|
|
231
|
+
uploadRoutingPolicies,
|
|
232
|
+
userSessions,
|
|
233
|
+
authHandoffs,
|
|
234
|
+
providerConfigs,
|
|
235
|
+
oauthStates,
|
|
236
|
+
connectedAccounts,
|
|
237
|
+
s3StorageConfigs,
|
|
238
|
+
storageAccounts,
|
|
239
|
+
folders,
|
|
240
|
+
files,
|
|
241
|
+
fileShares,
|
|
242
|
+
filePreviewTokens,
|
|
243
|
+
uploadSessions,
|
|
244
|
+
auditLogs,
|
|
245
|
+
workspaceInvites,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
const jsonString = JSON.stringify(backup, (_key, value) => {
|
|
249
|
+
if (typeof value === 'bigint')
|
|
250
|
+
return value.toString();
|
|
251
|
+
return value;
|
|
252
|
+
}, 2);
|
|
253
|
+
res.setHeader('Content-Disposition', `attachment; filename="9drive-backup-${dateStr}.json"`);
|
|
254
|
+
res.setHeader('Content-Type', 'application/json');
|
|
255
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
|
|
256
|
+
return res.send(jsonString);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return next(error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
exports.systemRouter.post('/restore', auth_middleware_js_1.requireAuth, (req, res, next) => {
|
|
263
|
+
try {
|
|
264
|
+
const contentType = req.headers['content-type'];
|
|
265
|
+
if (!contentType?.includes('multipart/form-data')) {
|
|
266
|
+
return res.status(400).json({ code: 'BAD_REQUEST', message: 'multipart/form-data required.' });
|
|
267
|
+
}
|
|
268
|
+
const busboy = (0, busboy_1.default)({ headers: req.headers, limits: { files: 1, fileSize: 100 * 1024 * 1024 } });
|
|
269
|
+
let fileReceived = false;
|
|
270
|
+
busboy.on('file', (name, fileStream, info) => {
|
|
271
|
+
fileReceived = true;
|
|
272
|
+
const filename = (info.filename || '').toLowerCase();
|
|
273
|
+
const chunks = [];
|
|
274
|
+
fileStream.on('data', (chunk) => {
|
|
275
|
+
chunks.push(chunk);
|
|
276
|
+
});
|
|
277
|
+
fileStream.on('end', async () => {
|
|
278
|
+
try {
|
|
279
|
+
const buffer = Buffer.concat(chunks);
|
|
280
|
+
if (buffer.length === 0) {
|
|
281
|
+
return res.status(400).json({ code: 'EMPTY_FILE', message: 'Uploaded backup file is empty.' });
|
|
282
|
+
}
|
|
283
|
+
const isSqlite = Boolean(process.env.DATABASE_URL?.startsWith('file:') || process.env.DATABASE_URL?.startsWith('sqlite:'));
|
|
284
|
+
// Handle SQLite .db upload
|
|
285
|
+
if (filename.endsWith('.db')) {
|
|
286
|
+
if (!isSqlite) {
|
|
287
|
+
return res.status(400).json({
|
|
288
|
+
code: 'INVALID_FORMAT',
|
|
289
|
+
message: 'You uploaded a SQLite .db file, but the active database is MySQL. Please upload a 9Drive JSON backup file (.json).'
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const dbPath = getDatabaseFilePath();
|
|
293
|
+
const tempDbPath = dbPath + '.tmp';
|
|
294
|
+
fs_1.default.writeFileSync(tempDbPath, buffer);
|
|
295
|
+
await prisma_js_1.prisma.$disconnect();
|
|
296
|
+
fs_1.default.renameSync(tempDbPath, dbPath);
|
|
297
|
+
return res.json({
|
|
298
|
+
status: 'success',
|
|
299
|
+
message: 'Database restored successfully from .db file. Please reload the application.'
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// Handle JSON backup upload
|
|
303
|
+
let parsed;
|
|
304
|
+
try {
|
|
305
|
+
parsed = JSON.parse(buffer.toString('utf-8'));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return res.status(400).json({
|
|
309
|
+
code: 'PARSE_ERROR',
|
|
310
|
+
message: 'Invalid backup file format. Expected a valid 9Drive JSON backup file.'
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const backupData = parsed.data || parsed;
|
|
314
|
+
if (!backupData || (!backupData.users && !backupData.connectedAccounts && !backupData.files && !backupData.folders)) {
|
|
315
|
+
return res.status(400).json({
|
|
316
|
+
code: 'INVALID_BACKUP_STRUCTURE',
|
|
317
|
+
message: 'Uploaded file does not contain valid 9Drive backup data.'
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
await restoreDatabaseFromJson(backupData, isSqlite);
|
|
321
|
+
return res.json({
|
|
322
|
+
status: 'success',
|
|
323
|
+
message: 'Database restored successfully! All tables and data have been restored.'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.error('Failed to restore database:', err);
|
|
328
|
+
return res.status(500).json({
|
|
329
|
+
code: 'RESTORE_FAILED',
|
|
330
|
+
message: 'Failed to restore database: ' + (err.message || 'Unknown error'),
|
|
331
|
+
error: err.message
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
fileStream.on('error', (err) => {
|
|
336
|
+
console.error('File stream error:', err);
|
|
337
|
+
if (!res.headersSent) {
|
|
338
|
+
return res.status(500).json({ code: 'UPLOAD_ERROR', message: err.message });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
busboy.on('error', (err) => {
|
|
343
|
+
console.error('Busboy error:', err);
|
|
344
|
+
if (!res.headersSent) {
|
|
345
|
+
next(err);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
busboy.on('finish', () => {
|
|
349
|
+
if (!fileReceived && !res.headersSent) {
|
|
350
|
+
return res.status(400).json({ code: 'BAD_REQUEST', message: 'No file uploaded.' });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
req.pipe(busboy);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
return next(error);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
async function restoreDatabaseFromJson(backupData, isSqlite) {
|
|
360
|
+
await prisma_js_1.prisma.$transaction(async (tx) => {
|
|
361
|
+
if (!isSqlite) {
|
|
362
|
+
await tx.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0;');
|
|
363
|
+
}
|
|
364
|
+
// Delete existing records in child-to-parent order
|
|
365
|
+
await tx.workspaceInvite.deleteMany({});
|
|
366
|
+
await tx.auditLog.deleteMany({});
|
|
367
|
+
await tx.uploadSession.deleteMany({});
|
|
368
|
+
await tx.filePreviewToken.deleteMany({});
|
|
369
|
+
await tx.fileShare.deleteMany({});
|
|
370
|
+
await tx.file.deleteMany({});
|
|
371
|
+
await tx.folder.deleteMany({});
|
|
372
|
+
await tx.storageAccount.deleteMany({});
|
|
373
|
+
await tx.s3StorageConfig.deleteMany({});
|
|
374
|
+
await tx.connectedAccount.deleteMany({});
|
|
375
|
+
await tx.oauthState.deleteMany({});
|
|
376
|
+
await tx.providerConfig.deleteMany({});
|
|
377
|
+
await tx.authHandoff.deleteMany({});
|
|
378
|
+
await tx.userSession.deleteMany({});
|
|
379
|
+
await tx.uploadRoutingPolicy.deleteMany({});
|
|
380
|
+
await tx.apiKey.deleteMany({});
|
|
381
|
+
await tx.user.deleteMany({});
|
|
382
|
+
// Insert restored records
|
|
383
|
+
if (backupData.users?.length) {
|
|
384
|
+
const records = backupData.users.map((u) => ({
|
|
385
|
+
id: u.id,
|
|
386
|
+
name: u.name,
|
|
387
|
+
email: u.email,
|
|
388
|
+
passwordHash: u.passwordHash,
|
|
389
|
+
status: u.status,
|
|
390
|
+
createdAt: u.createdAt ? new Date(u.createdAt) : new Date(),
|
|
391
|
+
updatedAt: u.updatedAt ? new Date(u.updatedAt) : new Date(),
|
|
392
|
+
}));
|
|
393
|
+
await tx.user.createMany({ data: records });
|
|
394
|
+
}
|
|
395
|
+
if (backupData.apiKeys?.length) {
|
|
396
|
+
const records = backupData.apiKeys.map((k) => ({
|
|
397
|
+
id: k.id,
|
|
398
|
+
userId: k.userId,
|
|
399
|
+
name: k.name,
|
|
400
|
+
keyPrefix: k.keyPrefix,
|
|
401
|
+
keyHash: k.keyHash,
|
|
402
|
+
scopes: k.scopes,
|
|
403
|
+
status: k.status,
|
|
404
|
+
lastUsedAt: k.lastUsedAt ? new Date(k.lastUsedAt) : null,
|
|
405
|
+
expiresAt: k.expiresAt ? new Date(k.expiresAt) : null,
|
|
406
|
+
revokedAt: k.revokedAt ? new Date(k.revokedAt) : null,
|
|
407
|
+
createdAt: k.createdAt ? new Date(k.createdAt) : new Date(),
|
|
408
|
+
updatedAt: k.updatedAt ? new Date(k.updatedAt) : new Date(),
|
|
409
|
+
}));
|
|
410
|
+
await tx.apiKey.createMany({ data: records });
|
|
411
|
+
}
|
|
412
|
+
if (backupData.uploadRoutingPolicies?.length) {
|
|
413
|
+
const records = backupData.uploadRoutingPolicies.map((p) => ({
|
|
414
|
+
id: p.id,
|
|
415
|
+
userId: p.userId,
|
|
416
|
+
mode: p.mode,
|
|
417
|
+
priorityAccountIds: p.priorityAccountIds,
|
|
418
|
+
roundRobinCursor: p.roundRobinCursor ?? 0,
|
|
419
|
+
createdAt: p.createdAt ? new Date(p.createdAt) : new Date(),
|
|
420
|
+
updatedAt: p.updatedAt ? new Date(p.updatedAt) : new Date(),
|
|
421
|
+
}));
|
|
422
|
+
await tx.uploadRoutingPolicy.createMany({ data: records });
|
|
423
|
+
}
|
|
424
|
+
if (backupData.userSessions?.length) {
|
|
425
|
+
const records = backupData.userSessions.map((s) => ({
|
|
426
|
+
id: s.id,
|
|
427
|
+
userId: s.userId,
|
|
428
|
+
refreshTokenHash: s.refreshTokenHash,
|
|
429
|
+
userAgent: s.userAgent ?? null,
|
|
430
|
+
ipAddress: s.ipAddress ?? null,
|
|
431
|
+
expiresAt: s.expiresAt ? new Date(s.expiresAt) : new Date(),
|
|
432
|
+
revokedAt: s.revokedAt ? new Date(s.revokedAt) : null,
|
|
433
|
+
createdAt: s.createdAt ? new Date(s.createdAt) : new Date(),
|
|
434
|
+
updatedAt: s.updatedAt ? new Date(s.updatedAt) : new Date(),
|
|
435
|
+
}));
|
|
436
|
+
await tx.userSession.createMany({ data: records });
|
|
437
|
+
}
|
|
438
|
+
if (backupData.authHandoffs?.length) {
|
|
439
|
+
const records = backupData.authHandoffs.map((h) => ({
|
|
440
|
+
id: h.id,
|
|
441
|
+
userId: h.userId,
|
|
442
|
+
tokenHash: h.tokenHash,
|
|
443
|
+
expiresAt: h.expiresAt ? new Date(h.expiresAt) : new Date(),
|
|
444
|
+
usedAt: h.usedAt ? new Date(h.usedAt) : null,
|
|
445
|
+
createdAt: h.createdAt ? new Date(h.createdAt) : new Date(),
|
|
446
|
+
}));
|
|
447
|
+
await tx.authHandoff.createMany({ data: records });
|
|
448
|
+
}
|
|
449
|
+
if (backupData.providerConfigs?.length) {
|
|
450
|
+
const records = backupData.providerConfigs.map((c) => ({
|
|
451
|
+
id: c.id,
|
|
452
|
+
userId: c.userId ?? null,
|
|
453
|
+
provider: c.provider,
|
|
454
|
+
clientIdEncrypted: c.clientIdEncrypted,
|
|
455
|
+
clientSecretEncrypted: c.clientSecretEncrypted,
|
|
456
|
+
redirectUri: c.redirectUri,
|
|
457
|
+
scopes: c.scopes,
|
|
458
|
+
status: c.status,
|
|
459
|
+
createdAt: c.createdAt ? new Date(c.createdAt) : new Date(),
|
|
460
|
+
updatedAt: c.updatedAt ? new Date(c.updatedAt) : new Date(),
|
|
461
|
+
}));
|
|
462
|
+
await tx.providerConfig.createMany({ data: records });
|
|
463
|
+
}
|
|
464
|
+
if (backupData.oauthStates?.length) {
|
|
465
|
+
const records = backupData.oauthStates.map((o) => ({
|
|
466
|
+
id: o.id,
|
|
467
|
+
userId: o.userId ?? null,
|
|
468
|
+
providerConfigId: o.providerConfigId,
|
|
469
|
+
flow: o.flow,
|
|
470
|
+
stateHash: o.stateHash,
|
|
471
|
+
expiresAt: o.expiresAt ? new Date(o.expiresAt) : new Date(),
|
|
472
|
+
usedAt: o.usedAt ? new Date(o.usedAt) : null,
|
|
473
|
+
createdAt: o.createdAt ? new Date(o.createdAt) : new Date(),
|
|
474
|
+
}));
|
|
475
|
+
await tx.oauthState.createMany({ data: records });
|
|
476
|
+
}
|
|
477
|
+
if (backupData.connectedAccounts?.length) {
|
|
478
|
+
const records = backupData.connectedAccounts.map((a) => ({
|
|
479
|
+
id: a.id,
|
|
480
|
+
userId: a.userId,
|
|
481
|
+
providerConfigId: a.providerConfigId ?? null,
|
|
482
|
+
provider: a.provider,
|
|
483
|
+
providerAccountId: a.providerAccountId,
|
|
484
|
+
email: a.email,
|
|
485
|
+
displayName: a.displayName ?? null,
|
|
486
|
+
avatarUrl: a.avatarUrl ?? null,
|
|
487
|
+
accessTokenEncrypted: a.accessTokenEncrypted ?? null,
|
|
488
|
+
refreshTokenEncrypted: a.refreshTokenEncrypted ?? null,
|
|
489
|
+
tokenExpiresAt: a.tokenExpiresAt ? new Date(a.tokenExpiresAt) : null,
|
|
490
|
+
scopes: a.scopes,
|
|
491
|
+
status: a.status,
|
|
492
|
+
lastError: a.lastError ?? null,
|
|
493
|
+
createdAt: a.createdAt ? new Date(a.createdAt) : new Date(),
|
|
494
|
+
updatedAt: a.updatedAt ? new Date(a.updatedAt) : new Date(),
|
|
495
|
+
}));
|
|
496
|
+
await tx.connectedAccount.createMany({ data: records });
|
|
497
|
+
}
|
|
498
|
+
if (backupData.s3StorageConfigs?.length) {
|
|
499
|
+
const records = backupData.s3StorageConfigs.map((s) => ({
|
|
500
|
+
id: s.id,
|
|
501
|
+
userId: s.userId,
|
|
502
|
+
connectedAccountId: s.connectedAccountId,
|
|
503
|
+
name: s.name,
|
|
504
|
+
bucket: s.bucket,
|
|
505
|
+
region: s.region,
|
|
506
|
+
endpoint: s.endpoint ?? null,
|
|
507
|
+
accessKeyIdEncrypted: s.accessKeyIdEncrypted,
|
|
508
|
+
secretAccessKeyEncrypted: s.secretAccessKeyEncrypted,
|
|
509
|
+
forcePathStyle: Boolean(s.forcePathStyle),
|
|
510
|
+
prefix: s.prefix ?? '9drive',
|
|
511
|
+
quotaBytes: s.quotaBytes != null ? BigInt(s.quotaBytes) : null,
|
|
512
|
+
status: s.status,
|
|
513
|
+
createdAt: s.createdAt ? new Date(s.createdAt) : new Date(),
|
|
514
|
+
updatedAt: s.updatedAt ? new Date(s.updatedAt) : new Date(),
|
|
515
|
+
}));
|
|
516
|
+
await tx.s3StorageConfig.createMany({ data: records });
|
|
517
|
+
}
|
|
518
|
+
if (backupData.storageAccounts?.length) {
|
|
519
|
+
const records = backupData.storageAccounts.map((s) => ({
|
|
520
|
+
id: s.id,
|
|
521
|
+
connectedAccountId: s.connectedAccountId,
|
|
522
|
+
totalBytes: s.totalBytes != null ? BigInt(s.totalBytes) : null,
|
|
523
|
+
usedBytes: s.usedBytes != null ? BigInt(s.usedBytes) : 0n,
|
|
524
|
+
availableBytes: s.availableBytes != null ? BigInt(s.availableBytes) : null,
|
|
525
|
+
trashBytes: s.trashBytes != null ? BigInt(s.trashBytes) : null,
|
|
526
|
+
lastSyncedAt: s.lastSyncedAt ? new Date(s.lastSyncedAt) : null,
|
|
527
|
+
createdAt: s.createdAt ? new Date(s.createdAt) : new Date(),
|
|
528
|
+
updatedAt: s.updatedAt ? new Date(s.updatedAt) : new Date(),
|
|
529
|
+
}));
|
|
530
|
+
await tx.storageAccount.createMany({ data: records });
|
|
531
|
+
}
|
|
532
|
+
if (backupData.folders?.length) {
|
|
533
|
+
const records = backupData.folders.map((f) => ({
|
|
534
|
+
id: f.id,
|
|
535
|
+
userId: f.userId,
|
|
536
|
+
parentId: f.parentId ?? null,
|
|
537
|
+
connectedAccountId: f.connectedAccountId ?? null,
|
|
538
|
+
provider: f.provider ?? 'google_drive',
|
|
539
|
+
providerFolderId: f.providerFolderId ?? null,
|
|
540
|
+
name: f.name,
|
|
541
|
+
color: f.color ?? 'text-blue-500',
|
|
542
|
+
iconUrl: f.iconUrl ?? null,
|
|
543
|
+
createdAt: f.createdAt ? new Date(f.createdAt) : new Date(),
|
|
544
|
+
updatedAt: f.updatedAt ? new Date(f.updatedAt) : new Date(),
|
|
545
|
+
deletedAt: f.deletedAt ? new Date(f.deletedAt) : null,
|
|
546
|
+
}));
|
|
547
|
+
await tx.folder.createMany({ data: records });
|
|
548
|
+
}
|
|
549
|
+
if (backupData.files?.length) {
|
|
550
|
+
const records = backupData.files.map((f) => ({
|
|
551
|
+
id: f.id,
|
|
552
|
+
userId: f.userId,
|
|
553
|
+
connectedAccountId: f.connectedAccountId,
|
|
554
|
+
folderId: f.folderId ?? null,
|
|
555
|
+
provider: f.provider,
|
|
556
|
+
providerFileId: f.providerFileId,
|
|
557
|
+
name: f.name,
|
|
558
|
+
mimeType: f.mimeType,
|
|
559
|
+
sizeBytes: f.sizeBytes != null ? BigInt(f.sizeBytes) : 0n,
|
|
560
|
+
checksum: f.checksum ?? null,
|
|
561
|
+
status: f.status ?? 'active',
|
|
562
|
+
createdAt: f.createdAt ? new Date(f.createdAt) : new Date(),
|
|
563
|
+
updatedAt: f.updatedAt ? new Date(f.updatedAt) : new Date(),
|
|
564
|
+
deletedAt: f.deletedAt ? new Date(f.deletedAt) : null,
|
|
565
|
+
}));
|
|
566
|
+
await tx.file.createMany({ data: records });
|
|
567
|
+
}
|
|
568
|
+
if (backupData.fileShares?.length) {
|
|
569
|
+
const records = backupData.fileShares.map((s) => ({
|
|
570
|
+
id: s.id,
|
|
571
|
+
fileId: s.fileId,
|
|
572
|
+
userId: s.userId,
|
|
573
|
+
token: s.token ?? null,
|
|
574
|
+
tokenHash: s.tokenHash,
|
|
575
|
+
enabled: s.enabled ?? true,
|
|
576
|
+
expiresAt: s.expiresAt ? new Date(s.expiresAt) : null,
|
|
577
|
+
createdAt: s.createdAt ? new Date(s.createdAt) : new Date(),
|
|
578
|
+
updatedAt: s.updatedAt ? new Date(s.updatedAt) : new Date(),
|
|
579
|
+
}));
|
|
580
|
+
await tx.fileShare.createMany({ data: records });
|
|
581
|
+
}
|
|
582
|
+
if (backupData.filePreviewTokens?.length) {
|
|
583
|
+
const records = backupData.filePreviewTokens.map((t) => ({
|
|
584
|
+
id: t.id,
|
|
585
|
+
fileId: t.fileId,
|
|
586
|
+
userId: t.userId,
|
|
587
|
+
tokenHash: t.tokenHash,
|
|
588
|
+
expiresAt: t.expiresAt ? new Date(t.expiresAt) : new Date(),
|
|
589
|
+
createdAt: t.createdAt ? new Date(t.createdAt) : new Date(),
|
|
590
|
+
}));
|
|
591
|
+
await tx.filePreviewToken.createMany({ data: records });
|
|
592
|
+
}
|
|
593
|
+
if (backupData.uploadSessions?.length) {
|
|
594
|
+
const records = backupData.uploadSessions.map((u) => ({
|
|
595
|
+
id: u.id,
|
|
596
|
+
userId: u.userId,
|
|
597
|
+
targetConnectedAccountId: u.targetConnectedAccountId ?? null,
|
|
598
|
+
folderId: u.folderId ?? null,
|
|
599
|
+
fileName: u.fileName,
|
|
600
|
+
mimeType: u.mimeType,
|
|
601
|
+
sizeBytes: u.sizeBytes != null ? BigInt(u.sizeBytes) : 0n,
|
|
602
|
+
status: u.status,
|
|
603
|
+
googleSessionUri: u.googleSessionUri ?? null,
|
|
604
|
+
errorMessage: u.errorMessage ?? null,
|
|
605
|
+
createdAt: u.createdAt ? new Date(u.createdAt) : new Date(),
|
|
606
|
+
completedAt: u.completedAt ? new Date(u.completedAt) : null,
|
|
607
|
+
}));
|
|
608
|
+
await tx.uploadSession.createMany({ data: records });
|
|
609
|
+
}
|
|
610
|
+
if (backupData.auditLogs?.length) {
|
|
611
|
+
const records = backupData.auditLogs.map((l) => ({
|
|
612
|
+
id: l.id,
|
|
613
|
+
userId: l.userId ?? null,
|
|
614
|
+
action: l.action,
|
|
615
|
+
entityType: l.entityType,
|
|
616
|
+
entityId: l.entityId ?? null,
|
|
617
|
+
metadata: l.metadata ?? null,
|
|
618
|
+
createdAt: l.createdAt ? new Date(l.createdAt) : new Date(),
|
|
619
|
+
}));
|
|
620
|
+
await tx.auditLog.createMany({ data: records });
|
|
621
|
+
}
|
|
622
|
+
if (backupData.workspaceInvites?.length) {
|
|
623
|
+
const records = backupData.workspaceInvites.map((i) => ({
|
|
624
|
+
id: i.id,
|
|
625
|
+
inviterId: i.inviterId,
|
|
626
|
+
inviteeEmail: i.inviteeEmail,
|
|
627
|
+
targetType: i.targetType ?? 'file',
|
|
628
|
+
targetId: i.targetId ?? '',
|
|
629
|
+
role: i.role ?? 'viewer',
|
|
630
|
+
status: i.status ?? 'pending',
|
|
631
|
+
revokedAt: i.revokedAt ? new Date(i.revokedAt) : null,
|
|
632
|
+
acceptedAt: i.acceptedAt ? new Date(i.acceptedAt) : null,
|
|
633
|
+
createdAt: i.createdAt ? new Date(i.createdAt) : new Date(),
|
|
634
|
+
updatedAt: i.updatedAt ? new Date(i.updatedAt) : new Date(),
|
|
635
|
+
}));
|
|
636
|
+
await tx.workspaceInvite.createMany({ data: records });
|
|
637
|
+
}
|
|
638
|
+
if (!isSqlite) {
|
|
639
|
+
await tx.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1;');
|
|
640
|
+
}
|
|
641
|
+
}, {
|
|
642
|
+
timeout: 60000,
|
|
643
|
+
maxWait: 10000,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function getDatabaseFilePath() {
|
|
647
|
+
const dbUrl = process.env.DATABASE_URL || 'file:./dev.db';
|
|
648
|
+
let cleanPath = dbUrl.replace(/^(sqlite|file):/, '');
|
|
649
|
+
if (cleanPath.includes('?')) {
|
|
650
|
+
cleanPath = cleanPath.split('?')[0];
|
|
651
|
+
}
|
|
652
|
+
if (!path_1.default.isAbsolute(cleanPath)) {
|
|
653
|
+
let baseDir = path_1.default.resolve(process.cwd(), 'prisma');
|
|
654
|
+
if (!fs_1.default.existsSync(baseDir)) {
|
|
655
|
+
baseDir = path_1.default.resolve(process.cwd(), 'backend', 'prisma');
|
|
656
|
+
}
|
|
657
|
+
if (!fs_1.default.existsSync(baseDir)) {
|
|
658
|
+
baseDir = path_1.default.resolve(process.cwd(), '..', 'backend', 'prisma');
|
|
659
|
+
}
|
|
660
|
+
return path_1.default.resolve(baseDir, cleanPath);
|
|
661
|
+
}
|
|
662
|
+
return cleanPath;
|
|
663
|
+
}
|