semuthitamku commited on
Commit
42596c9
·
verified ·
1 Parent(s): 7c3fa30

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +126 -30
index.js CHANGED
@@ -18,7 +18,7 @@ const app = express();
18
  app.use(express.json({ limit: "50mb" }));
19
  app.use(express.urlencoded({ extended: true }));
20
 
21
- // ROOT_DIR default ke os.tmpdir(), bisa override via env ROOT_DIR
22
  const ROOT_DIR = path.resolve(process.env.ROOT_DIR || os.tmpdir());
23
  fsExtra.ensureDirSync(ROOT_DIR);
24
 
@@ -27,19 +27,30 @@ const uploadTmpDir = path.join(os.tmpdir(), "filemgr_uploads");
27
  fsExtra.ensureDirSync(uploadTmpDir);
28
  const upload = multer({ dest: uploadTmpDir });
29
 
30
- // Utils
31
- const toPosix = (p) => p.replace(/\\/g, "/");
32
- const badName = (name) => !name || name.includes("..") || name.includes("/") || name.includes("\\");
 
 
 
 
 
 
 
 
 
33
  function safeResolve(rel = "") {
34
- const relNorm = String(rel).replace(/^[/\```+/, "");
35
  const full = path.resolve(ROOT_DIR, relNorm);
36
- if (!full.startsWith(ROOT_DIR)) {
 
37
  const err = new Error("Path outside ROOT_DIR");
38
  err.status = 400;
39
  throw err;
40
  }
41
  return full;
42
  }
 
43
  function buildBreadcrumb(rel = "") {
44
  const parts = toPosix(rel).split("/").filter(Boolean);
45
  let acc = "";
@@ -50,11 +61,13 @@ function buildBreadcrumb(rel = "") {
50
  }
51
  return crumbs;
52
  }
 
 
53
  function detectExtFull(name) {
54
- const lower = name.toLowerCase();
55
  if (lower.endsWith(".tar.gz")) return "tar.gz";
56
  if (lower.endsWith(".tgz")) return "tgz";
57
- return path.extname(lower).slice(1);
58
  }
59
  function archiveFormatFromName(name) {
60
  const e = detectExtFull(name);
@@ -64,14 +77,51 @@ function archiveFormatFromName(name) {
64
  return null;
65
  }
66
  function stripArchiveExt(baseName) {
67
- return baseName
68
- .replace(/\.tar\.gz$/i, "")
69
- .replace(/\.tgz$/i, "")
70
- .replace(/\.zip$/i, "")
71
- .replace(/\.tar$/i, "");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
- // Routes
75
  app.get("/api/list", async (req, res, next) => {
76
  try {
77
  const rel = req.query.path ? String(req.query.path) : "";
@@ -113,6 +163,7 @@ app.get("/api/list", async (req, res, next) => {
113
  }
114
  });
115
 
 
116
  app.post("/api/folder", async (req, res, next) => {
117
  try {
118
  const { parent = "", name } = req.body;
@@ -125,6 +176,7 @@ app.post("/api/folder", async (req, res, next) => {
125
  }
126
  });
127
 
 
128
  app.post("/api/file", async (req, res, next) => {
129
  try {
130
  const { parent = "", name, content = "" } = req.body;
@@ -139,6 +191,7 @@ app.post("/api/file", async (req, res, next) => {
139
  }
140
  });
141
 
 
142
  app.put("/api/file", async (req, res, next) => {
143
  try {
144
  const { path: rel, content = "" } = req.body;
@@ -153,6 +206,7 @@ app.put("/api/file", async (req, res, next) => {
153
  }
154
  });
155
 
 
156
  app.get("/api/file", async (req, res, next) => {
157
  try {
158
  const rel = String(req.query.path || "");
@@ -167,6 +221,7 @@ app.get("/api/file", async (req, res, next) => {
167
  }
168
  });
169
 
 
170
  app.post("/api/rename", async (req, res, next) => {
171
  try {
172
  const { path: rel, newName } = req.body;
@@ -181,6 +236,7 @@ app.post("/api/rename", async (req, res, next) => {
181
  }
182
  });
183
 
 
184
  app.post("/api/move", async (req, res, next) => {
185
  try {
186
  const { from, toDir } = req.body;
@@ -196,6 +252,7 @@ app.post("/api/move", async (req, res, next) => {
196
  }
197
  });
198
 
 
199
  app.delete("/api/entry", async (req, res, next) => {
200
  try {
201
  const rel = String(req.query.path || "");
@@ -207,6 +264,7 @@ app.delete("/api/entry", async (req, res, next) => {
207
  }
208
  });
209
 
 
210
  app.post("/api/upload", upload.array("files"), async (req, res, next) => {
211
  try {
212
  const rel = String(req.query.path || "");
@@ -224,39 +282,75 @@ app.post("/api/upload", upload.array("files"), async (req, res, next) => {
224
  }
225
  });
226
 
 
227
  app.post("/api/fetch-url", async (req, res, next) => {
228
  try {
229
- const { url, destDir = "", filename } = req.body;
230
  if (!url) return res.status(400).json({ error: "Missing url" });
231
  const destDirFull = safeResolve(destDir);
232
  await fsExtra.ensureDir(destDirFull);
233
 
 
 
 
 
 
234
  let name = filename;
235
- const resp = await axios.get(url, { responseType: "stream", validateStatus: (s) => s >= 200 && s < 400 });
236
  if (!name) {
237
  const cd = resp.headers["content-disposition"];
238
- if (cd) {
239
- const m = /filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i.exec(cd);
240
- if (m) name = decodeURIComponent(m[1]);
241
- }
242
  if (!name) {
243
  try {
244
  const u = new URL(url);
245
  const base = path.basename(u.pathname);
246
  name = base || "downloaded.file";
247
- } catch {}
 
 
248
  }
249
  }
250
  if (badName(name)) name = `downloaded-${Date.now()}`;
251
- const destFull = path.join(destDirFull, name);
252
- const ws = fs.createWriteStream(destFull);
 
 
 
253
  await pipeline(resp.data, ws);
254
- res.json({ ok: true, savedAs: toPosix(path.join(destDir, name)) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  } catch (err) {
256
  next(err);
257
  }
258
  });
259
 
 
260
  app.post("/api/archive", async (req, res, next) => {
261
  try {
262
  const { path: rel = "", entries = [], name, format = "zip" } = req.body;
@@ -285,7 +379,7 @@ app.post("/api/archive", async (req, res, next) => {
285
 
286
  let outName = name;
287
  if (!outName) {
288
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
289
  outName = `archive-${ts}.${extName}`;
290
  } else {
291
  const lower = outName.toLowerCase();
@@ -307,13 +401,13 @@ app.post("/api/archive", async (req, res, next) => {
307
  archive.on("error", reject);
308
  archive.pipe(output);
309
 
310
- for (const name of entries) {
311
- if (badName(path.basename(name))) {
312
  archive.destroy(new Error("Invalid entry name"));
313
  return;
314
  }
315
- const full = path.join(dirFull, name);
316
- const nameInArchive = path.basename(name);
317
  if (fs.existsSync(full)) {
318
  const stat = fs.lstatSync(full);
319
  if (stat.isDirectory()) archive.directory(full, nameInArchive);
@@ -329,6 +423,7 @@ app.post("/api/archive", async (req, res, next) => {
329
  }
330
  });
331
 
 
332
  app.post("/api/unarchive", async (req, res, next) => {
333
  try {
334
  const { zipPath, destDir } = req.body;
@@ -359,6 +454,7 @@ app.post("/api/unarchive", async (req, res, next) => {
359
  }
360
  });
361
 
 
362
  app.get("/api/download", async (req, res, next) => {
363
  try {
364
  const rel = String(req.query.path || "");
@@ -371,7 +467,7 @@ app.get("/api/download", async (req, res, next) => {
371
  }
372
  });
373
 
374
- // UI
375
  app.use("/", express.static(path.join(__dirname, "public")));
376
 
377
  app.use((err, req, res, next) => {
 
18
  app.use(express.json({ limit: "50mb" }));
19
  app.use(express.urlencoded({ extended: true }));
20
 
21
+ // ROOT_DIR default: os.tmpdir() (bisa override via env ROOT_DIR)
22
  const ROOT_DIR = path.resolve(process.env.ROOT_DIR || os.tmpdir());
23
  fsExtra.ensureDirSync(ROOT_DIR);
24
 
 
27
  fsExtra.ensureDirSync(uploadTmpDir);
28
  const upload = multer({ dest: uploadTmpDir });
29
 
30
+ // ===== Utils (tanpa regex untuk normalisasi path) =====
31
+ const toPosix = (p) => String(p).split("\\").join("/");
32
+
33
+ function stripLeadingSlashes(s) {
34
+ if (!s) return "";
35
+ let i = 0;
36
+ while (i < s.length && (s[i] === "/" || s[i] === "\\")) i++;
37
+ return s.slice(i);
38
+ }
39
+ const badName = (name) =>
40
+ !name || String(name).includes("..") || String(name).includes("/") || String(name).includes("\\");
41
+
42
  function safeResolve(rel = "") {
43
+ const relNorm = stripLeadingSlashes(String(rel));
44
  const full = path.resolve(ROOT_DIR, relNorm);
45
+ const relToRoot = path.relative(ROOT_DIR, full);
46
+ if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
47
  const err = new Error("Path outside ROOT_DIR");
48
  err.status = 400;
49
  throw err;
50
  }
51
  return full;
52
  }
53
+
54
  function buildBreadcrumb(rel = "") {
55
  const parts = toPosix(rel).split("/").filter(Boolean);
56
  let acc = "";
 
61
  }
62
  return crumbs;
63
  }
64
+
65
+ // Deteksi format arsip (tanpa regex)
66
  function detectExtFull(name) {
67
+ const lower = String(name).toLowerCase();
68
  if (lower.endsWith(".tar.gz")) return "tar.gz";
69
  if (lower.endsWith(".tgz")) return "tgz";
70
+ return path.extname(lower).slice(1); // zip/tar dll
71
  }
72
  function archiveFormatFromName(name) {
73
  const e = detectExtFull(name);
 
77
  return null;
78
  }
79
  function stripArchiveExt(baseName) {
80
+ let b = String(baseName);
81
+ const lower = b.toLowerCase();
82
+ if (lower.endsWith(".tar.gz")) return b.slice(0, -7);
83
+ if (lower.endsWith(".tgz")) return b.slice(0, -4);
84
+ if (lower.endsWith(".zip")) return b.slice(0, -4);
85
+ if (lower.endsWith(".tar")) return b.slice(0, -4);
86
+ return b;
87
+ }
88
+ function guessArchiveFormat(name, contentType = "") {
89
+ const byName = archiveFormatFromName(name);
90
+ if (byName) return byName;
91
+ const ct = String(contentType).toLowerCase();
92
+ if (ct.includes("zip")) return "zip";
93
+ if (ct.includes("x-tar") || ct === "application/tar") return "tar";
94
+ if (ct.includes("gzip")) return "tgz";
95
+ return null;
96
+ }
97
+ function parseContentDispositionFilename(cd) {
98
+ if (!cd) return null;
99
+ // Contoh: attachment; filename="file.zip"; filename*=UTF-8''file.zip
100
+ const parts = cd.split(";").map((s) => s.trim());
101
+ let fname = null;
102
+ for (const p of parts) {
103
+ if (p.toLowerCase().startsWith("filename*=")) {
104
+ let v = p.substring(9).trim(); // setelah 'filename*='
105
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
106
+ v = v.slice(1, -1);
107
+ }
108
+ const idx = v.indexOf("''");
109
+ if (idx !== -1) v = v.slice(idx + 2);
110
+ try { fname = decodeURIComponent(v); } catch { fname = v; }
111
+ break;
112
+ } else if (p.toLowerCase().startsWith("filename=")) {
113
+ let v = p.substring(9).trim();
114
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
115
+ v = v.slice(1, -1);
116
+ }
117
+ try { fname = decodeURIComponent(v); } catch { fname = v; }
118
+ // Jangan break dulu, utamakan filename* kalau ada (RFC)
119
+ }
120
+ }
121
+ return fname;
122
  }
123
 
124
+ // ===== Routes =====
125
  app.get("/api/list", async (req, res, next) => {
126
  try {
127
  const rel = req.query.path ? String(req.query.path) : "";
 
163
  }
164
  });
165
 
166
+ // Create folder
167
  app.post("/api/folder", async (req, res, next) => {
168
  try {
169
  const { parent = "", name } = req.body;
 
176
  }
177
  });
178
 
179
+ // Create file
180
  app.post("/api/file", async (req, res, next) => {
181
  try {
182
  const { parent = "", name, content = "" } = req.body;
 
191
  }
192
  });
193
 
194
+ // Update text file
195
  app.put("/api/file", async (req, res, next) => {
196
  try {
197
  const { path: rel, content = "" } = req.body;
 
206
  }
207
  });
208
 
209
+ // Read small text file
210
  app.get("/api/file", async (req, res, next) => {
211
  try {
212
  const rel = String(req.query.path || "");
 
221
  }
222
  });
223
 
224
+ // Rename
225
  app.post("/api/rename", async (req, res, next) => {
226
  try {
227
  const { path: rel, newName } = req.body;
 
236
  }
237
  });
238
 
239
+ // Move
240
  app.post("/api/move", async (req, res, next) => {
241
  try {
242
  const { from, toDir } = req.body;
 
252
  }
253
  });
254
 
255
+ // Delete
256
  app.delete("/api/entry", async (req, res, next) => {
257
  try {
258
  const rel = String(req.query.path || "");
 
264
  }
265
  });
266
 
267
+ // Upload
268
  app.post("/api/upload", upload.array("files"), async (req, res, next) => {
269
  try {
270
  const rel = String(req.query.path || "");
 
282
  }
283
  });
284
 
285
+ // Download dari URL + auto-extract (ZIP/TAR/TGZ)
286
  app.post("/api/fetch-url", async (req, res, next) => {
287
  try {
288
+ const { url, destDir = "", filename, autoExtract = true, removeArchive = false, extractTo } = req.body;
289
  if (!url) return res.status(400).json({ error: "Missing url" });
290
  const destDirFull = safeResolve(destDir);
291
  await fsExtra.ensureDir(destDirFull);
292
 
293
+ const resp = await axios.get(url, {
294
+ responseType: "stream",
295
+ validateStatus: (s) => s >= 200 && s < 400
296
+ });
297
+
298
  let name = filename;
 
299
  if (!name) {
300
  const cd = resp.headers["content-disposition"];
301
+ name = parseContentDispositionFilename(cd);
 
 
 
302
  if (!name) {
303
  try {
304
  const u = new URL(url);
305
  const base = path.basename(u.pathname);
306
  name = base || "downloaded.file";
307
+ } catch {
308
+ name = "downloaded.file";
309
+ }
310
  }
311
  }
312
  if (badName(name)) name = `downloaded-${Date.now()}`;
313
+ const savedRel = toPosix(path.join(destDir, name));
314
+ const savedFull = path.join(destDirFull, name);
315
+
316
+ // Simpan file
317
+ const ws = fs.createWriteStream(savedFull);
318
  await pipeline(resp.data, ws);
319
+
320
+ let extractedTo = null;
321
+ if (autoExtract) {
322
+ const format = guessArchiveFormat(name, resp.headers["content-type"]);
323
+ if (format) {
324
+ let destRel = extractTo;
325
+ if (!destRel) {
326
+ const base = stripArchiveExt(path.basename(name));
327
+ destRel = toPosix(path.join(destDir, base));
328
+ }
329
+ const destFull = safeResolve(destRel);
330
+ await fsExtra.ensureDir(destFull);
331
+
332
+ if (format === "zip") {
333
+ await extract(savedFull, { dir: destFull });
334
+ } else if (format === "tar") {
335
+ await tar.x({ file: savedFull, cwd: destFull, gzip: false });
336
+ } else if (format === "tgz") {
337
+ await tar.x({ file: savedFull, cwd: destFull, gzip: true });
338
+ }
339
+ extractedTo = destRel;
340
+
341
+ if (removeArchive) {
342
+ await fsExtra.remove(savedFull);
343
+ }
344
+ }
345
+ }
346
+
347
+ res.json({ ok: true, savedAs: savedRel, extractedTo });
348
  } catch (err) {
349
  next(err);
350
  }
351
  });
352
 
353
+ // Archive (zip/tar/tgz)
354
  app.post("/api/archive", async (req, res, next) => {
355
  try {
356
  const { path: rel = "", entries = [], name, format = "zip" } = req.body;
 
379
 
380
  let outName = name;
381
  if (!outName) {
382
+ const ts = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
383
  outName = `archive-${ts}.${extName}`;
384
  } else {
385
  const lower = outName.toLowerCase();
 
401
  archive.on("error", reject);
402
  archive.pipe(output);
403
 
404
+ for (const nm of entries) {
405
+ if (badName(path.basename(nm))) {
406
  archive.destroy(new Error("Invalid entry name"));
407
  return;
408
  }
409
+ const full = path.join(dirFull, nm);
410
+ const nameInArchive = path.basename(nm);
411
  if (fs.existsSync(full)) {
412
  const stat = fs.lstatSync(full);
413
  if (stat.isDirectory()) archive.directory(full, nameInArchive);
 
423
  }
424
  });
425
 
426
+ // Unarchive (zip/tar/tgz)
427
  app.post("/api/unarchive", async (req, res, next) => {
428
  try {
429
  const { zipPath, destDir } = req.body;
 
454
  }
455
  });
456
 
457
+ // Download file ke client
458
  app.get("/api/download", async (req, res, next) => {
459
  try {
460
  const rel = String(req.query.path || "");
 
467
  }
468
  });
469
 
470
+ // Serve UI
471
  app.use("/", express.static(path.join(__dirname, "public")));
472
 
473
  app.use((err, req, res, next) => {