semuthitamku commited on
Commit
7cbb886
·
verified ·
1 Parent(s): 6fb1aae

Rename main.coffee to index.js

Browse files
Files changed (2) hide show
  1. index.js +255 -0
  2. main.coffee +0 -58
index.js ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import express from 'express';
5
+ import serveIndex from 'serve-index';
6
+ import bytes from 'bytes';
7
+ import { fileTypeFromBuffer } from 'file-type';
8
+ import cloudcmd from 'cloudcmd';
9
+
10
+ const PORT = Number(process.env.PORT || 7860);
11
+ const LIMIT_SIZE = process.env.LIMIT_SIZE || '500mb';
12
+ const TMP_DIR = process.env.TMP_DIR || path.join(os.tmpdir());
13
+ const MAX_BYTES = typeof LIMIT_SIZE === 'string' ? bytes.parse(LIMIT_SIZE) : LIMIT_SIZE;
14
+
15
+ const toHuman = (n) =>
16
+ typeof bytes.format === 'function'
17
+ ? bytes.format(n, { unitSeparator: ' ' })
18
+ : bytes(n, { unitSeparator: ' ' });
19
+
20
+ await fs.mkdir(TMP_DIR, { recursive: true });
21
+
22
+ const app = express();
23
+ app.set('json spaces', 4);
24
+ app.use(express.json({ limit: LIMIT_SIZE }));
25
+ app.use(express.urlencoded({ extended: true, limit: LIMIT_SIZE }));
26
+
27
+ // Logger
28
+ app.use((req, _res, next) => {
29
+ const time = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
30
+ console.log(`[${time}] ${req.method}: ${req.url}`);
31
+ next();
32
+ });
33
+
34
+ // Static + directory listing
35
+ app.use('/file', express.static(TMP_DIR));
36
+ app.use('/file', serveIndex(TMP_DIR, { hidden: true, icons: true }));
37
+
38
+ app.use(
39
+ '/manage',
40
+ cloudcmd({
41
+ root: TMP_DIR,
42
+ prefix: '/manage',
43
+ })
44
+ );
45
+
46
+ // Helper: buat URL absolut untuk file
47
+ function fileUrl(req, fileName) {
48
+ const host = process.env.SPACE_HOST || req.get('host');
49
+ const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
50
+ return `${proto}://${host}/file/${encodeURIComponent(fileName)}`;
51
+ }
52
+
53
+ // Helper: decode base64 (dukungan data URL + base64url)
54
+ function decodeBase64ToBuffer(input) {
55
+ if (typeof input !== 'string') {
56
+ throw new Error('Input harus string base64');
57
+ }
58
+ let raw = input.trim();
59
+
60
+ // data:[mime];base64,<data>
61
+ const comma = raw.indexOf(',');
62
+ if (raw.startsWith('data:') && comma !== -1) {
63
+ raw = raw.slice(comma + 1);
64
+ }
65
+
66
+ raw = raw.replace(/\s+/g, '');
67
+ // dukung base64url
68
+ raw = raw.replace(/-/g, '+').replace(/_/g, '/');
69
+ const pad = raw.length % 4;
70
+ if (pad) raw += '='.repeat(4 - pad);
71
+
72
+ const buf = Buffer.from(raw, 'base64');
73
+ // validasi round-trip
74
+ const reEncoded = buf.toString('base64').replace(/=+$/, '');
75
+ const normalized = raw.replace(/=+$/, '');
76
+ if (reEncoded !== normalized) throw new Error('Base64 tidak valid');
77
+ return buf;
78
+ }
79
+
80
+ // FE sederhana
81
+ app.get('/', (_req, res) => {
82
+ res.type('html').send(`<!doctype html>
83
+ <html lang="id">
84
+ <head>
85
+ <meta charset="utf-8" />
86
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
87
+ <title>Downloader -> tmp</title>
88
+ <style>
89
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:720px;margin:40px auto;padding:0 16px;}
90
+ form{display:flex;gap:8px}
91
+ input[type=url]{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px}
92
+ button{padding:10px 14px;border:0;background:#0b5;color:#fff;border-radius:8px;cursor:pointer}
93
+ button:disabled{opacity:.6;cursor:not-allowed}
94
+ .result{margin-top:16px;white-space:pre-wrap;background:#f7f7f7;padding:12px;border-radius:8px;border:1px solid #eee}
95
+ .tips{margin-top:10px;font-size:.9em;color:#555}
96
+ a{color:#06c;text-decoration:none}
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <h1>Download ke tmp folder</h1>
101
+ <p class="tips">Limit ukuran: ${LIMIT_SIZE}. Lihat daftar file di <a href="/file" target="_blank">/file</a> atau kelola di <a href="/manage" target="_blank">/manage</a>.</p>
102
+
103
+ <form id="dl-form">
104
+ <input id="url" type="url" placeholder="https://contoh.com/file.jpg" required />
105
+ <button id="submit" type="submit">Download</button>
106
+ </form>
107
+
108
+ <div id="out" class="result" hidden></div>
109
+
110
+ <script>
111
+ const form = document.getElementById('dl-form');
112
+ const out = document.getElementById('out');
113
+ const btn = document.getElementById('submit');
114
+
115
+ form.addEventListener('submit', async (e) => {
116
+ e.preventDefault();
117
+ const url = document.getElementById('url').value.trim();
118
+ if (!url) return;
119
+
120
+ btn.disabled = true;
121
+ out.hidden = true;
122
+ out.textContent = '';
123
+
124
+ try {
125
+ const resp = await fetch('/download', {
126
+ method: 'POST',
127
+ headers: {'Content-Type': 'application/json'},
128
+ body: JSON.stringify({ url })
129
+ });
130
+ const data = await resp.json();
131
+ btn.disabled = false;
132
+ out.hidden = false;
133
+
134
+ if (!resp.ok) {
135
+ out.textContent = 'Error: ' + (data.message || resp.statusText);
136
+ return;
137
+ }
138
+
139
+ const link = data.url || ('/file/' + encodeURIComponent(data.name));
140
+ out.innerHTML = 'Saved as: <b>' + data.name + '</b> (' + data.size.readable + ')<br/>' +
141
+ 'Type: ' + (data.type && data.type.mime) + '<br/>' +
142
+ 'Open: <a href="' + link + '" target="_blank" rel="noopener">' + link + '</a>';
143
+ } catch (err) {
144
+ btn.disabled = false;
145
+ out.hidden = false;
146
+ out.textContent = 'Error: ' + err.message;
147
+ }
148
+ });
149
+ </script>
150
+ </body>
151
+ </html>`);
152
+ });
153
+
154
+ // Upload base64
155
+ app.post('/upload', async (req, res) => {
156
+ const { file } = req.body || {};
157
+ if (!file || typeof file !== 'string') {
158
+ return res.status(400).json({ message: 'Payload body "file" harus base64 string' });
159
+ }
160
+
161
+ try {
162
+ const fileBuffer = decodeBase64ToBuffer(file);
163
+ const ftype = (await fileTypeFromBuffer(fileBuffer)) || {
164
+ mime: 'application/octet-stream',
165
+ ext: 'bin',
166
+ };
167
+
168
+ const name = `${(ftype.mime.split('/')[0] || 'file')}-${Math.random().toString(36).slice(2)}.${ftype.ext}`;
169
+ await fs.writeFile(path.join(TMP_DIR, name), fileBuffer);
170
+
171
+ return res.json({
172
+ name,
173
+ size: { bytes: fileBuffer.length, readable: toHuman(fileBuffer.length) },
174
+ type: ftype,
175
+ url: fileUrl(req, name),
176
+ });
177
+ } catch (err) {
178
+ return res.status(400).json({ message: err.message || 'Gagal memproses base64' });
179
+ }
180
+ });
181
+
182
+ // Download by URL -> simpan ke TMP_DIR
183
+ app.post('/download', async (req, res) => {
184
+ const { url } = req.body || {};
185
+ if (!url || typeof url !== 'string') {
186
+ return res.status(400).json({ message: 'Body.url wajib diisi' });
187
+ }
188
+
189
+ let resp;
190
+ try {
191
+ resp = await fetch(url, { redirect: 'follow' });
192
+ } catch (err) {
193
+ return res.status(400).json({ message: `Gagal mengunduh URL: ${err.message}` });
194
+ }
195
+
196
+ if (!resp.ok) {
197
+ return res.status(resp.status).json({ message: `Gagal mengunduh (${resp.status} ${resp.statusText})` });
198
+ }
199
+
200
+ // Early check lewat Content-Length
201
+ const contentLengthHeader = resp.headers.get('content-length');
202
+ const contentLength = contentLengthHeader ? Number(contentLengthHeader) : null;
203
+ if (contentLength && contentLength > MAX_BYTES) {
204
+ return res.status(413).json({ message: `Ukuran file (${toHuman(contentLength)}) melebihi limit ${LIMIT_SIZE}` });
205
+ }
206
+
207
+ // Baca body ke Buffer dengan limit
208
+ if (!resp.body) {
209
+ return res.status(400).json({ message: 'Response tidak memiliki body' });
210
+ }
211
+
212
+ const reader = resp.body.getReader();
213
+ const chunks = [];
214
+ let total = 0;
215
+
216
+ try {
217
+ while (true) {
218
+ const { done, value } = await reader.read();
219
+ if (done) break;
220
+ total += value.byteLength;
221
+ if (total > MAX_BYTES) {
222
+ try { reader.cancel(); } catch {}
223
+ return res.status(413).json({ message: `Ukuran file melebihi limit ${LIMIT_SIZE}` });
224
+ }
225
+ chunks.push(Buffer.from(value));
226
+ }
227
+ } catch (err) {
228
+ try { reader.cancel(); } catch {}
229
+ return res.status(400).json({ message: `Gagal membaca stream: ${err.message}` });
230
+ }
231
+
232
+ const fileBuffer = Buffer.concat(chunks);
233
+
234
+ let ftype = await fileTypeFromBuffer(fileBuffer);
235
+ if (!ftype) ftype = { mime: 'application/octet-stream', ext: 'bin' };
236
+
237
+ const name = `${(ftype.mime.split('/')[0] || 'file')}-${Math.random().toString(36).slice(2)}.${ftype.ext}`;
238
+ await fs.writeFile(path.join(TMP_DIR, name), fileBuffer);
239
+
240
+ return res.json({
241
+ name,
242
+ size: { bytes: fileBuffer.length, readable: toHuman(fileBuffer.length) },
243
+ type: ftype,
244
+ url: fileUrl(req, name),
245
+ });
246
+ });
247
+
248
+ app.all('/', (_req, res, next) => next()); // biar GET / tetap ke FE
249
+
250
+ app.listen(PORT, () => {
251
+ console.log(`App running on port ${PORT}`);
252
+ console.log(`Files dir: ${TMP_DIR}`);
253
+ console.log(`Listing: http://localhost:${PORT}/file`);
254
+ console.log(`Manager: http://localhost:${PORT}/manage`);
255
+ });
main.coffee DELETED
@@ -1,58 +0,0 @@
1
- fs = require 'fs'
2
- os = require 'os'
3
- bytes = require 'bytes'
4
- express = require 'express'
5
- { fromBuffer } = require 'file-type'
6
- serve = require 'serve-index'
7
-
8
- limitSize = '500mb'
9
- tmpFolder = os.tmpdir()
10
-
11
- isBase64 = (str) ->
12
- try
13
- btoa(atob(str)) is str
14
- catch
15
- false
16
-
17
- app = express()
18
- app.set 'json spaces', 4
19
- # limit upload file
20
- app.use express.json limit: limitSize
21
- app.use express.urlencoded extended: true, limit: limitSize
22
- # logger
23
- app.use (req, res, next) ->
24
- time = new Date().toLocaleString 'id', timeZone: 'Asia/Jakarta'
25
- console.log "[#{time}] #{req.method}: #{req.url}"
26
- next()
27
- # allow user to access file in tmpFolder
28
- app.use '/file', express.static tmpFolder
29
- app.use '/file', serve tmpFolder, { hidden: true, icons: true }
30
-
31
- app.all '/', (_, res) -> res.send 'POST /upload'
32
-
33
- app.all '/upload', (req, res) ->
34
- if req.method isnt 'POST'
35
- res.json message: 'Method not allowed'
36
-
37
- { file } = req.body
38
- if not file and typeof file isnt 'string' and not isBase64 file
39
- res.json message: 'Payload body file must be filled in base64 format'
40
-
41
- fileBuffer = Buffer.from file, 'base64'
42
- ftype = await fromBuffer fileBuffer
43
- if not ftype then ftype = mime: 'file', ext: 'bin'
44
-
45
- randomName = Math.random().toString(36).slice(2)
46
- fileName = "#{ftype.mime.split('/')[0]}-#{randomName}.#{ftype.ext}"
47
- await fs.promises.writeFile "#{tmpFolder}/#{fileName}", fileBuffer
48
-
49
- res.json
50
- name: fileName,
51
- size:
52
- bytes: fileBuffer.length,
53
- readable: bytes fileBuffer.length, unitSeparator: ' '
54
- ,
55
- type: ftype,
56
- url: "https://#{process.env.SPACE_HOST}/file/#{fileName}"
57
-
58
- app.listen 7860, () -> console.log 'App running on port', 7860