@zmx0142857/file-share 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.
Files changed (4) hide show
  1. package/README.md +5 -0
  2. package/index.html +154 -0
  3. package/package.json +36 -0
  4. package/serve.js +54 -0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # file-share
2
+
3
+ ## Quick start
4
+
5
+ $ npx @zmx0142857/file-share
package/index.html ADDED
@@ -0,0 +1,154 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>文件分享</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; padding: 24px; }
10
+ .container { max-width: 720px; margin: 0 auto; }
11
+ h1 { text-align: center; margin-bottom: 24px; font-size: 28px; color: #1a1a2e; }
12
+ .upload-area { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
13
+ .upload-area input[type="file"] { display: none; }
14
+ .drop-zone { border: 2px dashed #c0c4cc; border-radius: 8px; padding: 32px; text-align: center; cursor: pointer; transition: border-color .2s, background .2s; }
15
+ .drop-zone:hover, .drop-zone.dragover { border-color: #409eff; background: #ecf5ff; }
16
+ .drop-zone p { color: #909399; font-size: 14px; margin-top: 8px; }
17
+ .drop-zone .icon { font-size: 40px; color: #c0c4cc; }
18
+ .file-names { margin-top: 12px; font-size: 13px; color: #606266; max-height: 80px; overflow-y: auto; }
19
+ .btn-row { margin-top: 16px; text-align: right; }
20
+ .btn { padding: 8px 24px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background .2s; }
21
+ .btn-primary { background: #409eff; color: #fff; }
22
+ .btn-primary:hover { background: #66b1ff; }
23
+ .btn-primary:disabled { background: #a0cfff; cursor: not-allowed; }
24
+ .msg { margin-top: 12px; font-size: 13px; padding: 8px 12px; border-radius: 4px; display: none; }
25
+ .msg.success { display: block; background: #f0f9eb; color: #67c23a; }
26
+ .msg.error { display: block; background: #fef0f0; color: #f56c6c; }
27
+ .file-list { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
28
+ .file-list h2 { font-size: 18px; margin-bottom: 16px; color: #1a1a2e; }
29
+ .file-list ul { list-style: none; }
30
+ .file-list li { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid #f0f0f0; }
31
+ .file-list li:last-child { border-bottom: none; }
32
+ .file-list li .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+ .file-list li .size { color: #909399; font-size: 12px; margin-left: 12px; white-space: nowrap; }
34
+ .file-list li a { color: #409eff; text-decoration: none; font-size: 13px; margin-left: 12px; white-space: nowrap; }
35
+ .file-list li a:hover { text-decoration: underline; }
36
+ .empty { text-align: center; color: #c0c4cc; padding: 24px 0; font-size: 14px; }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div class="container">
41
+ <h1>📁 文件分享</h1>
42
+ <div class="upload-area">
43
+ <input type="file" id="fileInput" multiple />
44
+ <div class="drop-zone" id="dropZone">
45
+ <div class="icon">⬆️</div>
46
+ <p>点击或拖拽文件到此处上传</p>
47
+ </div>
48
+ <div class="file-names" id="fileNames"></div>
49
+ <div class="btn-row">
50
+ <button class="btn btn-primary" id="uploadBtn" disabled>上传</button>
51
+ </div>
52
+ <div class="msg" id="msg"></div>
53
+ </div>
54
+ <div class="file-list">
55
+ <h2>📂 已分享文件</h2>
56
+ <ul id="fileList"></ul>
57
+ </div>
58
+ </div>
59
+ <script>
60
+ const fileInput = document.getElementById('fileInput');
61
+ const dropZone = document.getElementById('dropZone');
62
+ const fileNames = document.getElementById('fileNames');
63
+ const uploadBtn = document.getElementById('uploadBtn');
64
+ const msgEl = document.getElementById('msg');
65
+ const fileList = document.getElementById('fileList');
66
+
67
+ // 点击选择文件
68
+ dropZone.addEventListener('click', () => fileInput.click());
69
+ fileInput.addEventListener('change', () => {
70
+ showSelectedFiles();
71
+ uploadBtn.disabled = fileInput.files.length === 0;
72
+ });
73
+
74
+ // 拖拽
75
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
76
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
77
+ dropZone.addEventListener('drop', e => {
78
+ e.preventDefault();
79
+ dropZone.classList.remove('dragover');
80
+ fileInput.files = e.dataTransfer.files;
81
+ showSelectedFiles();
82
+ uploadBtn.disabled = fileInput.files.length === 0;
83
+ });
84
+
85
+ function showSelectedFiles() {
86
+ const names = Array.from(fileInput.files).map(f => f.name).join('、');
87
+ fileNames.textContent = names;
88
+ }
89
+
90
+ function showMsg(text, type) {
91
+ msgEl.textContent = text;
92
+ msgEl.className = 'msg ' + type;
93
+ }
94
+
95
+ // 上传
96
+ uploadBtn.addEventListener('click', async () => {
97
+ if (fileInput.files.length === 0) return;
98
+ uploadBtn.disabled = true;
99
+ uploadBtn.textContent = '上传中…';
100
+ const formData = new FormData();
101
+ Array.from(fileInput.files).forEach(f => formData.append('files', f));
102
+ try {
103
+ const resp = await fetch('/upload', { method: 'POST', body: formData });
104
+ const result = await resp.json();
105
+ if (result.code === 200) {
106
+ showMsg('上传成功!', 'success');
107
+ fileInput.value = '';
108
+ fileNames.textContent = '';
109
+ loadFileList();
110
+ } else {
111
+ showMsg(result.msg || '上传失败', 'error');
112
+ }
113
+ } catch (err) {
114
+ showMsg('网络错误:' + err.message, 'error');
115
+ } finally {
116
+ uploadBtn.textContent = '上传';
117
+ uploadBtn.disabled = fileInput.files.length === 0;
118
+ }
119
+ });
120
+
121
+ // 格式化文件大小
122
+ function formatSize(bytes) {
123
+ if (bytes === 0) return '0 B';
124
+ const k = 1024;
125
+ const units = ['B', 'KB', 'MB', 'GB'];
126
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
127
+ return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + units[i];
128
+ }
129
+
130
+ // 加载文件列表
131
+ async function loadFileList() {
132
+ try {
133
+ const resp = await fetch('/list');
134
+ const result = await resp.json();
135
+ if (result.code === 200 && result.data.length > 0) {
136
+ fileList.innerHTML = result.data.map(f =>
137
+ '<li>' +
138
+ '<span class="name" title="' + f.name + '">' + f.name + '</span>' +
139
+ '<span class="size">' + formatSize(f.size) + '</span>' +
140
+ '<a href="/files/' + encodeURIComponent(f.name) + '" target="_blank">下载</a>' +
141
+ '</li>'
142
+ ).join('');
143
+ } else {
144
+ fileList.innerHTML = '<div class="empty">暂无文件</div>';
145
+ }
146
+ } catch {
147
+ fileList.innerHTML = '<div class="empty">加载失败</div>';
148
+ }
149
+ }
150
+
151
+ loadFileList();
152
+ </script>
153
+ </body>
154
+ </html>
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@zmx0142857/file-share",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=16.0.0"
7
+ },
8
+ "dependencies": {
9
+ "cors": "^2.8.6",
10
+ "express": "^5.2.1",
11
+ "express-fileupload": "^1.5.2"
12
+ },
13
+ "description": "Share files on local network",
14
+ "main": "serve.js",
15
+ "bin": {
16
+ "file-share": "serve.js"
17
+ },
18
+ "scripts": {
19
+ "test": "echo \"Error: no test specified\" && exit 1"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/zmx0142857/file-share.git"
24
+ },
25
+ "keywords": [
26
+ "fs",
27
+ "upload",
28
+ "express"
29
+ ],
30
+ "author": "zmx0142857",
31
+ "license": "ISC",
32
+ "bugs": {
33
+ "url": "https://github.com/zmx0142857/file-share/issues"
34
+ },
35
+ "homepage": "https://github.com/zmx0142857/file-share#readme"
36
+ }
package/serve.js ADDED
@@ -0,0 +1,54 @@
1
+ import express from 'express'
2
+ import cors from 'cors'
3
+ import fileUpload from 'express-fileupload'
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+
7
+ const app = express()
8
+ app.use(cors())
9
+ app.use(fileUpload({ createParentPath: true, useTempFiles: true }))
10
+ app.use(express.json())
11
+ app.use('/files', express.static('files'))
12
+
13
+ // 文件列表接口
14
+ app.get('/list', (req, res) => {
15
+ const dir = path.resolve('files')
16
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
17
+ fs.readdir(dir, (err, entries) => {
18
+ if (err) return res.status(500).json({ code: 500, msg: '读取目录失败' })
19
+ const data = entries
20
+ .filter(name => fs.statSync(path.join(dir, name)).isFile())
21
+ .map(name => ({
22
+ name,
23
+ size: fs.statSync(path.join(dir, name)).size,
24
+ }))
25
+ res.json({ code: 200, msg: 'ok', data })
26
+ })
27
+ })
28
+
29
+ // 上传接口(返回 JSON)
30
+ app.post('/upload', (req, res) => {
31
+ if (!req.files || !req.files.files) {
32
+ return res.status(400).json({ code: 400, msg: '请选择一个或多个文件' })
33
+ }
34
+ let { files } = req.files
35
+ if (!Array.isArray(files)) files = [files]
36
+ const data = []
37
+ files.forEach(file => {
38
+ file.name = new TextDecoder().decode(Uint8Array.from(file.name, c => c.charCodeAt(0)))
39
+ console.log(file)
40
+ file.mv('./files/' + file.name)
41
+ data.push({
42
+ name: file.name,
43
+ mimetype: file.mimetype,
44
+ size: file.size,
45
+ })
46
+ })
47
+ res.json({ code: 200, msg: 'ok', data })
48
+ })
49
+
50
+ app.get('/', (req, res) => res.send(fs.readFileSync('index.html', 'utf-8')))
51
+
52
+ const argv = process.argv.slice(2)
53
+ const port = argv[0] || 3001
54
+ app.listen(port, () => console.log(`server is running at port http://localhost:${port}`))