byte-drop 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 +21 -0
- package/bun.lock +234 -0
- package/package.json +22 -0
- package/public/ByteDrop-logo.png +0 -0
- package/public/ByteDrop.png +0 -0
- package/public/app.js +194 -0
- package/public/index.html +60 -0
- package/public/receive.html +33 -0
- package/public/styles.css +406 -0
- package/readme.md +128 -0
- package/server.js +190 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhishek A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"express": "^5.2.1",
|
|
8
|
+
"multer": "^2.1.1",
|
|
9
|
+
"qrcode": "^1.5.4",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
|
15
|
+
|
|
16
|
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
17
|
+
|
|
18
|
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
19
|
+
|
|
20
|
+
"append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="],
|
|
21
|
+
|
|
22
|
+
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
|
23
|
+
|
|
24
|
+
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
|
25
|
+
|
|
26
|
+
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
|
27
|
+
|
|
28
|
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
|
29
|
+
|
|
30
|
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
31
|
+
|
|
32
|
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
|
33
|
+
|
|
34
|
+
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
|
35
|
+
|
|
36
|
+
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
|
37
|
+
|
|
38
|
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
|
39
|
+
|
|
40
|
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
|
41
|
+
|
|
42
|
+
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
|
43
|
+
|
|
44
|
+
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
|
45
|
+
|
|
46
|
+
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
|
47
|
+
|
|
48
|
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
49
|
+
|
|
50
|
+
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
|
51
|
+
|
|
52
|
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
53
|
+
|
|
54
|
+
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
|
55
|
+
|
|
56
|
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
|
57
|
+
|
|
58
|
+
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
|
59
|
+
|
|
60
|
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
|
61
|
+
|
|
62
|
+
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
|
63
|
+
|
|
64
|
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
65
|
+
|
|
66
|
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
|
67
|
+
|
|
68
|
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
69
|
+
|
|
70
|
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
71
|
+
|
|
72
|
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
|
73
|
+
|
|
74
|
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
|
75
|
+
|
|
76
|
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
|
77
|
+
|
|
78
|
+
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
|
79
|
+
|
|
80
|
+
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
|
81
|
+
|
|
82
|
+
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
|
83
|
+
|
|
84
|
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
85
|
+
|
|
86
|
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
|
87
|
+
|
|
88
|
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
|
89
|
+
|
|
90
|
+
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
|
91
|
+
|
|
92
|
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
|
93
|
+
|
|
94
|
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
|
95
|
+
|
|
96
|
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
97
|
+
|
|
98
|
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
99
|
+
|
|
100
|
+
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
|
101
|
+
|
|
102
|
+
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
|
103
|
+
|
|
104
|
+
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
|
105
|
+
|
|
106
|
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
|
107
|
+
|
|
108
|
+
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
|
109
|
+
|
|
110
|
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
|
111
|
+
|
|
112
|
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
|
113
|
+
|
|
114
|
+
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
|
115
|
+
|
|
116
|
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
|
117
|
+
|
|
118
|
+
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
|
119
|
+
|
|
120
|
+
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
|
121
|
+
|
|
122
|
+
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
|
123
|
+
|
|
124
|
+
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
|
125
|
+
|
|
126
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
127
|
+
|
|
128
|
+
"multer": ["multer@2.1.1", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "type-is": "^1.6.18" } }, "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A=="],
|
|
129
|
+
|
|
130
|
+
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
|
131
|
+
|
|
132
|
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
|
133
|
+
|
|
134
|
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
|
135
|
+
|
|
136
|
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
137
|
+
|
|
138
|
+
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
|
139
|
+
|
|
140
|
+
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
|
141
|
+
|
|
142
|
+
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
|
143
|
+
|
|
144
|
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
|
145
|
+
|
|
146
|
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
|
147
|
+
|
|
148
|
+
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
|
149
|
+
|
|
150
|
+
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
|
151
|
+
|
|
152
|
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
|
153
|
+
|
|
154
|
+
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": "bin/qrcode" }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
|
155
|
+
|
|
156
|
+
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
|
|
157
|
+
|
|
158
|
+
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
|
159
|
+
|
|
160
|
+
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
|
161
|
+
|
|
162
|
+
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
|
163
|
+
|
|
164
|
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
|
165
|
+
|
|
166
|
+
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
|
167
|
+
|
|
168
|
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
|
169
|
+
|
|
170
|
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
171
|
+
|
|
172
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
173
|
+
|
|
174
|
+
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
|
175
|
+
|
|
176
|
+
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
|
177
|
+
|
|
178
|
+
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
|
179
|
+
|
|
180
|
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
|
181
|
+
|
|
182
|
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
|
183
|
+
|
|
184
|
+
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
|
185
|
+
|
|
186
|
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
|
187
|
+
|
|
188
|
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
|
189
|
+
|
|
190
|
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
|
191
|
+
|
|
192
|
+
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
|
193
|
+
|
|
194
|
+
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
195
|
+
|
|
196
|
+
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
|
197
|
+
|
|
198
|
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
199
|
+
|
|
200
|
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
|
201
|
+
|
|
202
|
+
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
|
203
|
+
|
|
204
|
+
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
|
205
|
+
|
|
206
|
+
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
|
207
|
+
|
|
208
|
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
|
209
|
+
|
|
210
|
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
|
211
|
+
|
|
212
|
+
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
|
213
|
+
|
|
214
|
+
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
|
215
|
+
|
|
216
|
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
217
|
+
|
|
218
|
+
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
|
219
|
+
|
|
220
|
+
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
|
221
|
+
|
|
222
|
+
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
|
223
|
+
|
|
224
|
+
"multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
|
225
|
+
|
|
226
|
+
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
|
227
|
+
|
|
228
|
+
"multer/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
|
229
|
+
|
|
230
|
+
"multer/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
|
231
|
+
|
|
232
|
+
"multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
|
233
|
+
}
|
|
234
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "byte-drop",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Instant local-network file sharing with a QR-powered receive page.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"byte-drop": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js",
|
|
11
|
+
"audit": "npm audit --omit=dev"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"express": "^5.2.1",
|
|
18
|
+
"multer": "^2.1.1",
|
|
19
|
+
"qrcode": "^1.5.4"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
|
Binary file
|
|
Binary file
|
package/public/app.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const page = document.body.dataset.page;
|
|
2
|
+
const filesEl = document.getElementById("files");
|
|
3
|
+
const statusEl = document.getElementById("status");
|
|
4
|
+
const downloadAllBtn = document.getElementById("downloadAllBtn");
|
|
5
|
+
const clearBtn = document.getElementById("clearBtn");
|
|
6
|
+
const fileInput = document.getElementById("fileInput");
|
|
7
|
+
const drop = document.getElementById("drop");
|
|
8
|
+
|
|
9
|
+
function setStatus(message) {
|
|
10
|
+
if (statusEl) statusEl.textContent = message;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatBytes(bytes) {
|
|
14
|
+
if (bytes === 0) return "0 B";
|
|
15
|
+
|
|
16
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
17
|
+
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
18
|
+
const value = bytes / (1024 ** index);
|
|
19
|
+
|
|
20
|
+
return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createButton(label, className, onClick) {
|
|
24
|
+
const button = document.createElement("button");
|
|
25
|
+
button.type = "button";
|
|
26
|
+
button.className = `file-action ${className || ""}`.trim();
|
|
27
|
+
button.textContent = label;
|
|
28
|
+
button.addEventListener("click", onClick);
|
|
29
|
+
return button;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function uploadFiles(fileList) {
|
|
33
|
+
const files = Array.from(fileList);
|
|
34
|
+
if (!files.length) return;
|
|
35
|
+
|
|
36
|
+
const form = new FormData();
|
|
37
|
+
files.forEach(file => form.append("files", file));
|
|
38
|
+
setStatus(`Uploading ${files.length} file${files.length === 1 ? "" : "s"}...`);
|
|
39
|
+
|
|
40
|
+
const response = await fetch("/upload", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: form,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const payload = await response.json().catch(() => ({}));
|
|
47
|
+
throw new Error(payload.error || "Upload failed");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setStatus("Upload complete.");
|
|
51
|
+
await loadFiles();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderEmpty() {
|
|
55
|
+
const empty = document.createElement("div");
|
|
56
|
+
empty.className = "empty";
|
|
57
|
+
empty.textContent = page === "receive"
|
|
58
|
+
? "Waiting for shared files. Keep this tab open and they will appear automatically."
|
|
59
|
+
: "Nothing shared yet. Drop files above or choose them from your computer.";
|
|
60
|
+
filesEl.replaceChildren(empty);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderFiles(files) {
|
|
64
|
+
const cards = files.map(file => {
|
|
65
|
+
const card = document.createElement("article");
|
|
66
|
+
card.className = "file-card";
|
|
67
|
+
|
|
68
|
+
const details = document.createElement("div");
|
|
69
|
+
const name = document.createElement("span");
|
|
70
|
+
name.className = "file-name";
|
|
71
|
+
name.textContent = file.displayName || file.name;
|
|
72
|
+
|
|
73
|
+
const meta = document.createElement("span");
|
|
74
|
+
meta.className = "file-meta";
|
|
75
|
+
meta.textContent = `${formatBytes(file.size)} shared ${new Date(file.modifiedAt).toLocaleTimeString([], {
|
|
76
|
+
hour: "2-digit",
|
|
77
|
+
minute: "2-digit",
|
|
78
|
+
})}`;
|
|
79
|
+
|
|
80
|
+
details.append(name, meta);
|
|
81
|
+
|
|
82
|
+
const actions = document.createElement("div");
|
|
83
|
+
actions.className = "file-actions";
|
|
84
|
+
actions.append(createButton("Download", "", () => {
|
|
85
|
+
window.location.href = file.downloadUrl;
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
if (page === "send") {
|
|
89
|
+
actions.append(createButton("Delete", "danger", async () => {
|
|
90
|
+
await fetch(`/delete/${encodeURIComponent(file.name)}`, { method: "DELETE" });
|
|
91
|
+
await loadFiles();
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
card.append(details, actions);
|
|
96
|
+
return card;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
filesEl.replaceChildren(...cards);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadFiles() {
|
|
103
|
+
const response = await fetch("/files", { cache: "no-store" });
|
|
104
|
+
if (!response.ok) throw new Error("Could not load files");
|
|
105
|
+
|
|
106
|
+
const files = await response.json();
|
|
107
|
+
const hasFiles = files.length > 0;
|
|
108
|
+
|
|
109
|
+
if (clearBtn) clearBtn.hidden = !hasFiles;
|
|
110
|
+
if (downloadAllBtn) {
|
|
111
|
+
downloadAllBtn.hidden = !hasFiles;
|
|
112
|
+
downloadAllBtn.textContent = `Download all (${files.length})`;
|
|
113
|
+
downloadAllBtn.onclick = () => {
|
|
114
|
+
files.forEach((file, index) => {
|
|
115
|
+
setTimeout(() => window.open(file.downloadUrl, "_blank", "noopener"), index * 250);
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setStatus(hasFiles
|
|
121
|
+
? `${files.length} file${files.length === 1 ? "" : "s"} ready.`
|
|
122
|
+
: page === "receive" ? "Waiting for shared files..." : "Ready when you are.");
|
|
123
|
+
|
|
124
|
+
if (hasFiles) renderFiles(files);
|
|
125
|
+
else renderEmpty();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function loadQr() {
|
|
129
|
+
const qrBox = document.getElementById("qrBox");
|
|
130
|
+
const link = document.getElementById("link");
|
|
131
|
+
if (!qrBox || !link) return;
|
|
132
|
+
|
|
133
|
+
const response = await fetch("/qr", { cache: "no-store" });
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
|
|
136
|
+
const image = document.createElement("img");
|
|
137
|
+
image.src = data.qr;
|
|
138
|
+
image.alt = "QR code for the ByteDrop receive page";
|
|
139
|
+
|
|
140
|
+
qrBox.replaceChildren(image);
|
|
141
|
+
link.textContent = data.url;
|
|
142
|
+
link.href = data.url;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (fileInput) {
|
|
146
|
+
fileInput.addEventListener("change", async () => {
|
|
147
|
+
try {
|
|
148
|
+
await uploadFiles(fileInput.files);
|
|
149
|
+
fileInput.value = "";
|
|
150
|
+
} catch (error) {
|
|
151
|
+
setStatus(error.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (drop) {
|
|
157
|
+
["dragenter", "dragover"].forEach(eventName => {
|
|
158
|
+
drop.addEventListener(eventName, event => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
drop.classList.add("is-active");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
["dragleave", "drop"].forEach(eventName => {
|
|
165
|
+
drop.addEventListener(eventName, event => {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
drop.classList.remove("is-active");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
drop.addEventListener("drop", async event => {
|
|
172
|
+
try {
|
|
173
|
+
await uploadFiles(event.dataTransfer.files);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
setStatus(error.message);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
window.addEventListener("dragover", event => event.preventDefault());
|
|
180
|
+
window.addEventListener("drop", event => event.preventDefault());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (clearBtn) {
|
|
184
|
+
clearBtn.addEventListener("click", async () => {
|
|
185
|
+
await fetch("/clear", { method: "DELETE" });
|
|
186
|
+
await loadFiles();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
loadQr().catch(() => setStatus("Could not load the QR code."));
|
|
191
|
+
loadFiles().catch(() => setStatus("Could not load shared files."));
|
|
192
|
+
setInterval(() => {
|
|
193
|
+
loadFiles().catch(() => setStatus("Could not refresh shared files."));
|
|
194
|
+
}, 2500);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ByteDrop</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/ByteDrop.png">
|
|
8
|
+
<link rel="stylesheet" href="/styles.css">
|
|
9
|
+
<script src="/app.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body data-page="send">
|
|
12
|
+
<main class="shell">
|
|
13
|
+
<section class="hero" aria-labelledby="title">
|
|
14
|
+
<div class="brand">
|
|
15
|
+
<img src="/ByteDrop-logo.png" alt="ByteDrop" class="logo">
|
|
16
|
+
<p class="eyebrow">Local network handoff</p>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="hero-grid">
|
|
20
|
+
<div class="intro">
|
|
21
|
+
<h1 id="title">Drop files across the room.</h1>
|
|
22
|
+
<p>Share files from this computer to nearby devices on the same Wi-Fi. No cloud storage, no accounts, no public links.</p>
|
|
23
|
+
|
|
24
|
+
<div class="actions">
|
|
25
|
+
<label class="button primary" for="fileInput">Choose files</label>
|
|
26
|
+
<button class="button ghost" id="clearBtn" type="button" hidden>Clear queue</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<input type="file" id="fileInput" multiple hidden>
|
|
30
|
+
<p class="status" id="status" role="status" aria-live="polite">Ready when you are.</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<aside class="qr-panel" aria-label="Phone connection">
|
|
34
|
+
<div class="qr-frame" id="qrBox"></div>
|
|
35
|
+
<p class="qr-title">Scan to receive</p>
|
|
36
|
+
<a class="qr-link" id="link" href="/receive">/receive</a>
|
|
37
|
+
</aside>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section class="drop-zone" id="drop" tabindex="0" aria-label="Upload files by dropping them here">
|
|
42
|
+
<span class="drop-kicker">Drag files here</span>
|
|
43
|
+
<strong>Release to upload instantly</strong>
|
|
44
|
+
<small>Uploads stay on this computer until you delete them or stop sharing.</small>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section class="files-panel" aria-labelledby="filesTitle">
|
|
48
|
+
<div class="section-heading">
|
|
49
|
+
<div>
|
|
50
|
+
<p class="eyebrow">Shared now</p>
|
|
51
|
+
<h2 id="filesTitle">Files</h2>
|
|
52
|
+
</div>
|
|
53
|
+
<button class="button secondary" id="downloadAllBtn" type="button" hidden>Download all</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="file-list" id="files"></div>
|
|
57
|
+
</section>
|
|
58
|
+
</main>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Receive with ByteDrop</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/ByteDrop.png">
|
|
8
|
+
<link rel="stylesheet" href="/styles.css">
|
|
9
|
+
<script src="/app.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body data-page="receive">
|
|
12
|
+
<main class="shell mobile-shell">
|
|
13
|
+
<section class="receive-hero" aria-labelledby="title">
|
|
14
|
+
<img src="/ByteDrop-logo.png" alt="ByteDrop" class="logo compact">
|
|
15
|
+
<p class="eyebrow">Ready to receive</p>
|
|
16
|
+
<h1 id="title">Files from your computer appear here.</h1>
|
|
17
|
+
<p class="status" id="status" role="status" aria-live="polite">Checking for shared files...</p>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section class="files-panel" aria-labelledby="filesTitle">
|
|
21
|
+
<div class="section-heading">
|
|
22
|
+
<div>
|
|
23
|
+
<p class="eyebrow">Available now</p>
|
|
24
|
+
<h2 id="filesTitle">Downloads</h2>
|
|
25
|
+
</div>
|
|
26
|
+
<button class="button primary" id="downloadAllBtn" type="button" hidden>Download all</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="file-list" id="files"></div>
|
|
30
|
+
</section>
|
|
31
|
+
</main>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--ink: #f4efe4;
|
|
4
|
+
--muted: #a59d8d;
|
|
5
|
+
--paper: #10100e;
|
|
6
|
+
--panel: #1b1a16;
|
|
7
|
+
--panel-strong: #242219;
|
|
8
|
+
--line: #3a3529;
|
|
9
|
+
--accent: #f0c15d;
|
|
10
|
+
--accent-strong: #ffdb79;
|
|
11
|
+
--danger: #ef5b45;
|
|
12
|
+
--ok: #7ce0a6;
|
|
13
|
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
|
14
|
+
font-family: "Trebuchet MS", "Aptos", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
html {
|
|
22
|
+
min-height: 100%;
|
|
23
|
+
background:
|
|
24
|
+
radial-gradient(circle at 14% 10%, rgba(240, 193, 93, 0.18), transparent 28rem),
|
|
25
|
+
radial-gradient(circle at 88% 0%, rgba(124, 224, 166, 0.13), transparent 24rem),
|
|
26
|
+
linear-gradient(135deg, #0e0e0d 0%, #17150f 48%, #0d0d0c 100%);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
margin: 0;
|
|
32
|
+
color: var(--ink);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body::before {
|
|
36
|
+
content: "";
|
|
37
|
+
position: fixed;
|
|
38
|
+
inset: 0;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
opacity: 0.16;
|
|
41
|
+
background-image:
|
|
42
|
+
linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
|
43
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
|
44
|
+
background-size: 42px 42px;
|
|
45
|
+
mask-image: linear-gradient(to bottom, #000, transparent 74%);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
button,
|
|
49
|
+
input {
|
|
50
|
+
font: inherit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
a {
|
|
54
|
+
color: inherit;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.shell {
|
|
58
|
+
width: min(1120px, calc(100% - 32px));
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
padding: 32px 0 48px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.hero,
|
|
64
|
+
.receive-hero,
|
|
65
|
+
.files-panel {
|
|
66
|
+
border: 1px solid var(--line);
|
|
67
|
+
background: linear-gradient(145deg, rgba(27, 26, 22, 0.92), rgba(16, 16, 14, 0.8));
|
|
68
|
+
box-shadow: var(--shadow);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.hero {
|
|
72
|
+
min-height: 460px;
|
|
73
|
+
padding: clamp(22px, 4vw, 46px);
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
position: relative;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.hero::after {
|
|
80
|
+
content: "";
|
|
81
|
+
position: absolute;
|
|
82
|
+
right: -80px;
|
|
83
|
+
bottom: -130px;
|
|
84
|
+
width: 360px;
|
|
85
|
+
height: 360px;
|
|
86
|
+
border: 1px solid rgba(240, 193, 93, 0.34);
|
|
87
|
+
transform: rotate(18deg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.brand {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
gap: 20px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.logo {
|
|
98
|
+
width: min(310px, 62vw);
|
|
99
|
+
height: auto;
|
|
100
|
+
object-fit: contain;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.logo.compact {
|
|
104
|
+
width: min(260px, 78vw);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.eyebrow {
|
|
108
|
+
margin: 0 0 8px;
|
|
109
|
+
color: var(--accent);
|
|
110
|
+
font-size: 0.78rem;
|
|
111
|
+
font-weight: 800;
|
|
112
|
+
letter-spacing: 0.16em;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.hero-grid {
|
|
117
|
+
position: relative;
|
|
118
|
+
z-index: 1;
|
|
119
|
+
display: grid;
|
|
120
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
121
|
+
gap: clamp(22px, 5vw, 72px);
|
|
122
|
+
align-items: end;
|
|
123
|
+
margin-top: 44px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
h1,
|
|
127
|
+
h2 {
|
|
128
|
+
margin: 0;
|
|
129
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
130
|
+
font-weight: 700;
|
|
131
|
+
line-height: 0.98;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
h1 {
|
|
135
|
+
max-width: 720px;
|
|
136
|
+
font-size: clamp(3rem, 9vw, 7.4rem);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
h2 {
|
|
140
|
+
font-size: clamp(2rem, 5vw, 3.8rem);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.intro p:not(.eyebrow),
|
|
144
|
+
.receive-hero > p:not(.eyebrow),
|
|
145
|
+
.status {
|
|
146
|
+
max-width: 620px;
|
|
147
|
+
color: var(--muted);
|
|
148
|
+
font-size: 1.08rem;
|
|
149
|
+
line-height: 1.65;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.actions,
|
|
153
|
+
.section-heading {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
gap: 14px;
|
|
158
|
+
flex-wrap: wrap;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.actions {
|
|
162
|
+
justify-content: flex-start;
|
|
163
|
+
margin-top: 28px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.button {
|
|
167
|
+
min-height: 44px;
|
|
168
|
+
border: 1px solid transparent;
|
|
169
|
+
border-radius: 999px;
|
|
170
|
+
padding: 0 20px;
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
gap: 8px;
|
|
175
|
+
color: var(--paper);
|
|
176
|
+
background: var(--accent);
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
font-weight: 800;
|
|
179
|
+
text-decoration: none;
|
|
180
|
+
transition: transform 160ms ease, background 160ms ease, border-color 160ms ease;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.button:hover {
|
|
184
|
+
transform: translateY(-1px);
|
|
185
|
+
background: var(--accent-strong);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.button:focus-visible,
|
|
189
|
+
.drop-zone:focus-visible,
|
|
190
|
+
.file-action:focus-visible {
|
|
191
|
+
outline: 3px solid rgba(240, 193, 93, 0.46);
|
|
192
|
+
outline-offset: 3px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.button.ghost {
|
|
196
|
+
color: var(--ink);
|
|
197
|
+
background: transparent;
|
|
198
|
+
border-color: var(--line);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.button.ghost:hover {
|
|
202
|
+
background: rgba(239, 91, 69, 0.1);
|
|
203
|
+
border-color: rgba(239, 91, 69, 0.65);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.button.secondary {
|
|
207
|
+
color: var(--ink);
|
|
208
|
+
background: var(--panel-strong);
|
|
209
|
+
border-color: var(--line);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.qr-panel {
|
|
213
|
+
padding: 18px;
|
|
214
|
+
border: 1px solid var(--line);
|
|
215
|
+
border-radius: 8px;
|
|
216
|
+
background: rgba(244, 239, 228, 0.06);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.qr-frame {
|
|
220
|
+
display: grid;
|
|
221
|
+
min-height: 236px;
|
|
222
|
+
place-items: center;
|
|
223
|
+
border-radius: 6px;
|
|
224
|
+
background: #fff;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.qr-frame img {
|
|
228
|
+
width: 210px;
|
|
229
|
+
max-width: 100%;
|
|
230
|
+
height: auto;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.qr-title {
|
|
234
|
+
margin: 14px 0 6px;
|
|
235
|
+
color: var(--ink);
|
|
236
|
+
font-weight: 800;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.qr-link {
|
|
240
|
+
color: var(--muted);
|
|
241
|
+
font-size: 0.92rem;
|
|
242
|
+
overflow-wrap: anywhere;
|
|
243
|
+
text-decoration: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.drop-zone {
|
|
247
|
+
margin: 24px 0;
|
|
248
|
+
padding: clamp(28px, 8vw, 70px);
|
|
249
|
+
border: 2px dashed rgba(240, 193, 93, 0.38);
|
|
250
|
+
border-radius: 8px;
|
|
251
|
+
background: rgba(16, 16, 14, 0.7);
|
|
252
|
+
display: grid;
|
|
253
|
+
gap: 10px;
|
|
254
|
+
place-items: center;
|
|
255
|
+
text-align: center;
|
|
256
|
+
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.drop-zone.is-active {
|
|
260
|
+
transform: translateY(-2px);
|
|
261
|
+
border-color: var(--accent);
|
|
262
|
+
background: rgba(240, 193, 93, 0.1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.drop-kicker {
|
|
266
|
+
color: var(--accent);
|
|
267
|
+
font-weight: 800;
|
|
268
|
+
text-transform: uppercase;
|
|
269
|
+
letter-spacing: 0.14em;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.drop-zone strong {
|
|
273
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
274
|
+
font-size: clamp(2rem, 6vw, 4.8rem);
|
|
275
|
+
line-height: 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.drop-zone small {
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.files-panel {
|
|
283
|
+
padding: clamp(20px, 4vw, 34px);
|
|
284
|
+
border-radius: 8px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.file-list {
|
|
288
|
+
display: grid;
|
|
289
|
+
gap: 12px;
|
|
290
|
+
margin-top: 24px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.file-card {
|
|
294
|
+
display: grid;
|
|
295
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
296
|
+
gap: 16px;
|
|
297
|
+
align-items: center;
|
|
298
|
+
padding: 14px;
|
|
299
|
+
border: 1px solid rgba(244, 239, 228, 0.12);
|
|
300
|
+
border-radius: 8px;
|
|
301
|
+
background: rgba(244, 239, 228, 0.05);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.file-name {
|
|
305
|
+
display: block;
|
|
306
|
+
color: var(--ink);
|
|
307
|
+
font-weight: 800;
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
text-overflow: ellipsis;
|
|
310
|
+
white-space: nowrap;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.file-meta {
|
|
314
|
+
display: block;
|
|
315
|
+
margin-top: 4px;
|
|
316
|
+
color: var(--muted);
|
|
317
|
+
font-size: 0.9rem;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.file-actions {
|
|
321
|
+
display: flex;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.file-action {
|
|
326
|
+
min-width: 42px;
|
|
327
|
+
min-height: 38px;
|
|
328
|
+
border: 1px solid var(--line);
|
|
329
|
+
border-radius: 999px;
|
|
330
|
+
color: var(--ink);
|
|
331
|
+
background: transparent;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.file-action:hover {
|
|
336
|
+
border-color: var(--accent);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.file-action.danger:hover {
|
|
340
|
+
color: #fff;
|
|
341
|
+
border-color: var(--danger);
|
|
342
|
+
background: rgba(239, 91, 69, 0.2);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.empty {
|
|
346
|
+
padding: 30px;
|
|
347
|
+
border: 1px dashed var(--line);
|
|
348
|
+
border-radius: 8px;
|
|
349
|
+
color: var(--muted);
|
|
350
|
+
text-align: center;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.receive-hero {
|
|
354
|
+
padding: 28px;
|
|
355
|
+
border-radius: 8px;
|
|
356
|
+
text-align: left;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.receive-hero h1 {
|
|
360
|
+
font-size: clamp(2.7rem, 14vw, 5.2rem);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.mobile-shell {
|
|
364
|
+
width: min(760px, calc(100% - 24px));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@media (max-width: 760px) {
|
|
368
|
+
.shell {
|
|
369
|
+
width: min(100% - 20px, 720px);
|
|
370
|
+
padding-top: 12px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.brand,
|
|
374
|
+
.hero-grid {
|
|
375
|
+
display: grid;
|
|
376
|
+
grid-template-columns: 1fr;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.hero {
|
|
380
|
+
min-height: unset;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
h1 {
|
|
384
|
+
font-size: clamp(3rem, 16vw, 5.5rem);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.qr-panel {
|
|
388
|
+
width: 100%;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.drop-zone {
|
|
392
|
+
margin: 14px 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.file-card {
|
|
396
|
+
grid-template-columns: 1fr;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.file-actions {
|
|
400
|
+
justify-content: stretch;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.file-action {
|
|
404
|
+
flex: 1;
|
|
405
|
+
}
|
|
406
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ByteDrop
|
|
2
|
+
|
|
3
|
+
ByteDrop is a tiny local-network file drop for moving files from your computer to nearby devices on the same Wi-Fi. Start it, open the desktop page, scan the QR code from your phone, and download the shared files directly from your machine.
|
|
4
|
+
|
|
5
|
+
No accounts. No cloud storage. No public upload service.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- QR-powered receive page for phones and tablets
|
|
10
|
+
- Drag-and-drop or file picker uploads from the desktop page
|
|
11
|
+
- Live file list refresh on both sender and receiver screens
|
|
12
|
+
- One-click individual downloads or batch download opening
|
|
13
|
+
- Clear and delete controls for the sender
|
|
14
|
+
- Hardened local file handling with safe filenames, bounded upload size, and path validation
|
|
15
|
+
- Security headers for the static UI
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 18 or newer
|
|
20
|
+
- Devices connected to the same trusted local network
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Run without installing globally:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx byte-drop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install this checkout locally:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
npm start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If installed globally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g byte-drop
|
|
41
|
+
byte-drop
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
1. Start ByteDrop:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. Open the laptop URL printed in the terminal, usually:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
http://192.168.x.x:3000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. Drop or choose files on the desktop page.
|
|
59
|
+
|
|
60
|
+
4. Scan the QR code from your phone, or open:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
http://192.168.x.x:3000/receive
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
5. Download files on the receiving device.
|
|
67
|
+
|
|
68
|
+
Uploaded files are stored in the local `uploads/` folder and are ignored by git.
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
ByteDrop uses conservative defaults, and you can tune them with environment variables:
|
|
73
|
+
|
|
74
|
+
| Variable | Default | Purpose |
|
|
75
|
+
| --- | ---: | --- |
|
|
76
|
+
| `PORT` | `3000` | HTTP port for the local server |
|
|
77
|
+
| `BYTEDROP_MAX_FILE_SIZE` | `262144000` | Maximum size per file in bytes, 250 MB by default |
|
|
78
|
+
| `BYTEDROP_MAX_FILES_PER_REQUEST` | `20` | Maximum files accepted in one upload request |
|
|
79
|
+
|
|
80
|
+
PowerShell example:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
$env:PORT="4000"
|
|
84
|
+
$env:BYTEDROP_MAX_FILE_SIZE="104857600"
|
|
85
|
+
npm start
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Security Notes
|
|
89
|
+
|
|
90
|
+
ByteDrop is designed for trusted local networks, not the public internet. Anyone on the same reachable network who can open the ByteDrop URL can view and download files while the server is running.
|
|
91
|
+
|
|
92
|
+
This version includes:
|
|
93
|
+
|
|
94
|
+
- Generated storage names to avoid trusting user-provided filenames
|
|
95
|
+
- Filename sanitization and strict route validation
|
|
96
|
+
- Resolved-path checks for download and delete operations
|
|
97
|
+
- Upload count and file-size limits
|
|
98
|
+
- Hidden Express implementation header
|
|
99
|
+
- Content Security Policy and basic browser hardening headers
|
|
100
|
+
- Static file serving that denies dotfiles
|
|
101
|
+
|
|
102
|
+
Recommended use:
|
|
103
|
+
|
|
104
|
+
- Run it only on private networks you trust.
|
|
105
|
+
- Stop the server when sharing is done.
|
|
106
|
+
- Delete files from the UI when they should no longer be available.
|
|
107
|
+
- Avoid exposing the port through router forwarding, tunnels, or public hosting.
|
|
108
|
+
|
|
109
|
+
Run an npm dependency audit with:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run audit
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Project Structure
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
server.js Express server, upload handling, QR endpoint
|
|
119
|
+
public/index.html Sender interface
|
|
120
|
+
public/receive.html Receiver interface
|
|
121
|
+
public/styles.css Shared visual system
|
|
122
|
+
public/app.js Shared client behavior
|
|
123
|
+
uploads/ Runtime file storage, ignored by git
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
package/server.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const express = require("express");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const multer = require("multer");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const QRCode = require("qrcode");
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
const ROOT_DIR = __dirname;
|
|
13
|
+
const PUBLIC_DIR = path.join(ROOT_DIR, "public");
|
|
14
|
+
const UPLOAD_DIR = path.join(ROOT_DIR, "uploads");
|
|
15
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
16
|
+
const MAX_FILE_SIZE = Number(process.env.BYTEDROP_MAX_FILE_SIZE) || 250 * 1024 * 1024;
|
|
17
|
+
const MAX_FILES_PER_REQUEST = Number(process.env.BYTEDROP_MAX_FILES_PER_REQUEST) || 20;
|
|
18
|
+
|
|
19
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
20
|
+
|
|
21
|
+
app.disable("x-powered-by");
|
|
22
|
+
app.use((req, res, next) => {
|
|
23
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
24
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
25
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
26
|
+
res.setHeader(
|
|
27
|
+
"Content-Security-Policy",
|
|
28
|
+
"default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'"
|
|
29
|
+
);
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.use(express.static(PUBLIC_DIR, {
|
|
34
|
+
dotfiles: "deny",
|
|
35
|
+
etag: true,
|
|
36
|
+
index: "index.html",
|
|
37
|
+
maxAge: "1h",
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
function sanitizeOriginalName(name) {
|
|
41
|
+
const base = path.basename(name || "file");
|
|
42
|
+
const cleaned = base
|
|
43
|
+
.normalize("NFKD")
|
|
44
|
+
.replace(/[^\w.\- ]+/g, "")
|
|
45
|
+
.replace(/\s+/g, "-")
|
|
46
|
+
.replace(/^\.+/, "")
|
|
47
|
+
.slice(0, 120);
|
|
48
|
+
|
|
49
|
+
return cleaned || "file";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isManagedFileName(name) {
|
|
53
|
+
return /^[a-f0-9]{16}-[\w.\- ]{1,120}$/.test(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveUploadPath(name) {
|
|
57
|
+
if (!isManagedFileName(name)) return null;
|
|
58
|
+
|
|
59
|
+
const resolved = path.resolve(UPLOAD_DIR, name);
|
|
60
|
+
return resolved.startsWith(UPLOAD_DIR + path.sep) ? resolved : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getFileList() {
|
|
64
|
+
return fs.readdirSync(UPLOAD_DIR, { withFileTypes: true })
|
|
65
|
+
.filter(entry => entry.isFile() && isManagedFileName(entry.name))
|
|
66
|
+
.map(entry => {
|
|
67
|
+
const filePath = path.join(UPLOAD_DIR, entry.name);
|
|
68
|
+
const stats = fs.statSync(filePath);
|
|
69
|
+
return {
|
|
70
|
+
name: entry.name,
|
|
71
|
+
displayName: entry.name.replace(/^[a-f0-9]{16}-/, ""),
|
|
72
|
+
size: stats.size,
|
|
73
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
74
|
+
downloadUrl: `/download/${encodeURIComponent(entry.name)}`,
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const storage = multer.diskStorage({
|
|
81
|
+
destination: (_req, _file, cb) => cb(null, UPLOAD_DIR),
|
|
82
|
+
filename: (_req, file, cb) => {
|
|
83
|
+
const token = crypto.randomBytes(8).toString("hex");
|
|
84
|
+
cb(null, `${token}-${sanitizeOriginalName(file.originalname)}`);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const upload = multer({
|
|
89
|
+
storage,
|
|
90
|
+
limits: {
|
|
91
|
+
fileSize: MAX_FILE_SIZE,
|
|
92
|
+
files: MAX_FILES_PER_REQUEST,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post("/upload", upload.array("files", MAX_FILES_PER_REQUEST), (req, res) => {
|
|
97
|
+
res.status(201).json({ files: req.files.map(file => file.filename) });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
app.get("/files", (_req, res) => {
|
|
101
|
+
res.json(getFileList());
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
app.get("/download/:name", (req, res) => {
|
|
105
|
+
const filePath = resolveUploadPath(req.params.name);
|
|
106
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
107
|
+
res.sendStatus(404);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
res.download(filePath, req.params.name.replace(/^[a-f0-9]{16}-/, ""));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.get("/receive", (_req, res) => {
|
|
115
|
+
res.sendFile(path.join(PUBLIC_DIR, "receive.html"));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
app.delete("/clear", (_req, res) => {
|
|
119
|
+
for (const file of getFileList()) {
|
|
120
|
+
const filePath = resolveUploadPath(file.name);
|
|
121
|
+
if (filePath) fs.unlinkSync(filePath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
res.sendStatus(204);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.delete("/delete/:name", (req, res) => {
|
|
128
|
+
const filePath = resolveUploadPath(req.params.name);
|
|
129
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
130
|
+
res.sendStatus(404);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fs.unlinkSync(filePath);
|
|
135
|
+
res.sendStatus(204);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function getLocalIP() {
|
|
139
|
+
const nets = os.networkInterfaces();
|
|
140
|
+
for (const name of Object.keys(nets)) {
|
|
141
|
+
for (const net of nets[name] || []) {
|
|
142
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
143
|
+
return net.address;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return "localhost";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const IP = getLocalIP();
|
|
151
|
+
const BASE_URL = `http://${IP}:${PORT}`;
|
|
152
|
+
const QR_URL = `${BASE_URL}/receive`;
|
|
153
|
+
|
|
154
|
+
app.get("/qr", async (_req, res, next) => {
|
|
155
|
+
try {
|
|
156
|
+
const qr = await QRCode.toDataURL(QR_URL, {
|
|
157
|
+
color: {
|
|
158
|
+
dark: "#101010",
|
|
159
|
+
light: "#ffffff",
|
|
160
|
+
},
|
|
161
|
+
margin: 1,
|
|
162
|
+
width: 320,
|
|
163
|
+
});
|
|
164
|
+
res.json({ url: QR_URL, qr });
|
|
165
|
+
} catch (error) {
|
|
166
|
+
next(error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.use((error, _req, res, _next) => {
|
|
171
|
+
if (error instanceof multer.MulterError) {
|
|
172
|
+
res.status(400).json({ error: error.message });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.error(error);
|
|
177
|
+
res.status(500).json({ error: "Something went wrong." });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
app.listen(PORT, () => {
|
|
181
|
+
console.log(`
|
|
182
|
+
ByteDrop started
|
|
183
|
+
|
|
184
|
+
Laptop: ${BASE_URL}
|
|
185
|
+
Phone: ${QR_URL}
|
|
186
|
+
|
|
187
|
+
Open the laptop URL to upload files, then scan the QR code from your phone.
|
|
188
|
+
Press Ctrl+C to stop.
|
|
189
|
+
`);
|
|
190
|
+
});
|