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 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
+ });