share-home 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/.next/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +19 -0
- package/.next/build-manifest.json +20 -0
- package/.next/dev/build-manifest.json +18 -0
- package/.next/dev/cache/webpack/client-development/0.pack.gz +0 -0
- package/.next/dev/cache/webpack/client-development/index.pack.gz +0 -0
- package/.next/dev/cache/webpack/server-development/0.pack.gz +0 -0
- package/.next/dev/cache/webpack/server-development/index.pack.gz +0 -0
- package/.next/dev/react-loadable-manifest.json +1 -0
- package/.next/dev/server/app/api/config/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/documents/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/init/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/peers/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/page_client-reference-manifest.js +1 -0
- package/.next/dev/server/app-paths-manifest.json +3 -0
- package/.next/dev/server/middleware-build-manifest.js +18 -0
- package/.next/dev/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/dev/server/next-font-manifest.js +1 -0
- package/.next/dev/server/next-font-manifest.json +1 -0
- package/.next/dev/server/pages-manifest.json +1 -0
- package/.next/dev/server/server-reference-manifest.js +1 -0
- package/.next/dev/server/server-reference-manifest.json +5 -0
- package/.next/dev/server/vendor-chunks/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.4_react@19.2.4__react@19.2.4.js +3998 -0
- package/.next/dev/server/webpack-runtime.js +209 -0
- package/.next/dev/static/development/_buildManifest.js +1 -0
- package/.next/dev/static/development/_ssgManifest.js +1 -0
- package/.next/dev/types/app/layout.ts +87 -0
- package/.next/dev/types/app/page.ts +87 -0
- package/.next/dev/types/package.json +1 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +68 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +114 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.js +337 -0
- package/.next/required-server-files.json +337 -0
- package/.next/routes-manifest.json +147 -0
- package/.next/server/app/_global-error/page.js +32 -0
- package/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_global-error.html +1 -0
- package/.next/server/app/_global-error.meta +16 -0
- package/.next/server/app/_global-error.rsc +14 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +14 -0
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_index.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
- package/.next/server/app/_not-found/page.js +7 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +6 -0
- package/.next/server/app/_not-found.meta +16 -0
- package/.next/server/app/_not-found.rsc +18 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
- package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
- package/.next/server/app/_not-found.segments/_index.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/api/board/sync/route.js +1 -0
- package/.next/server/app/api/board/sync/route.js.nft.json +1 -0
- package/.next/server/app/api/board/sync/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/config/route.js +1 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/config/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/documents/route.js +1 -0
- package/.next/server/app/api/documents/route.js.nft.json +1 -0
- package/.next/server/app/api/documents/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/documents/sync/route.js +1 -0
- package/.next/server/app/api/documents/sync/route.js.nft.json +1 -0
- package/.next/server/app/api/documents/sync/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/init/route.js +1 -0
- package/.next/server/app/api/init/route.js.nft.json +1 -0
- package/.next/server/app/api/init/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/peers/route.js +1 -0
- package/.next/server/app/api/peers/route.js.nft.json +1 -0
- package/.next/server/app/api/peers/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/download/route.js +1 -0
- package/.next/server/app/api/transfer/download/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/prepare/route.js +1 -0
- package/.next/server/app/api/transfer/prepare/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/request/route.js +1 -0
- package/.next/server/app/api/transfer/request/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/request/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/shared/download/route.js +1 -0
- package/.next/server/app/api/transfer/shared/download/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/shared/route.js +1 -0
- package/.next/server/app/api/transfer/shared/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/start/route.js +1 -0
- package/.next/server/app/api/transfer/start/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/start/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/tasks/route.js +1 -0
- package/.next/server/app/api/transfer/tasks/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/index.html +6 -0
- package/.next/server/app/index.meta +14 -0
- package/.next/server/app/index.rsc +21 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/index.segments/_full.segment.rsc +21 -0
- package/.next/server/app/index.segments/_head.segment.rsc +6 -0
- package/.next/server/app/index.segments/_index.segment.rsc +5 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +5 -0
- package/.next/server/app/page.js +115 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +19 -0
- package/.next/server/chunks/31.js +2 -0
- package/.next/server/chunks/4.js +1 -0
- package/.next/server/chunks/404.js +1 -0
- package/.next/server/chunks/516.js +1 -0
- package/.next/server/chunks/718.js +45 -0
- package/.next/server/chunks/887.js +1 -0
- package/.next/server/chunks/891.js +18 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +6 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/prefetch-hints.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/JAumJsHdasa7bQ7jxx37q/_buildManifest.js +1 -0
- package/.next/static/JAumJsHdasa7bQ7jxx37q/_ssgManifest.js +1 -0
- package/.next/static/chunks/148-2d58b90f6dc8cfaf.js +32 -0
- package/.next/static/chunks/5d8f0495-d87c92750ebe0885.js +82 -0
- package/.next/static/chunks/649-20578e0ca00dbde1.js +14 -0
- package/.next/static/chunks/991cd08a-0938e33045166413.js +1 -0
- package/.next/static/chunks/app/_global-error/page-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/_not-found/page-5696c6e9b39a4885.js +1 -0
- package/.next/static/chunks/app/api/board/sync/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/config/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/documents/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/documents/sync/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/init/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/peers/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/download/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/prepare/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/request/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/shared/download/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/shared/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/start/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/tasks/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/layout-f1c1109b37ee9a67.js +1 -0
- package/.next/static/chunks/app/page-d667dde9ae730a9e.js +15 -0
- package/.next/static/chunks/be838f7e-d7eb8d1a464523ea.js +1 -0
- package/.next/static/chunks/framework-1af2d653ea416252.js +1 -0
- package/.next/static/chunks/main-app-2475c374c5ac40b5.js +1 -0
- package/.next/static/chunks/main-c04fb4d60a5182d4.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-9771ee9f95e5e628.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-1bf2fae924e39757.js +1 -0
- package/.next/static/css/30baeec9f6889470.css +1 -0
- package/.next/static/css/30f58f83a0192172.css +1 -0
- package/.next/static/media/0f1bdadaf30e2d5f-s.woff2 +0 -0
- package/.next/static/media/22a5144ee8d83bca-s.p.woff2 +0 -0
- package/.next/static/media/2c34d62a75506231-s.woff2 +0 -0
- package/.next/static/media/601f5c280d60caca-s.woff2 +0 -0
- package/.next/static/media/9766a7e9e2e0ad5a-s.woff2 +0 -0
- package/.next/static/media/a115172161b307bb-s.woff2 +0 -0
- package/.next/static/media/aa016aab0e6d1295-s.woff2 +0 -0
- package/.next/static/media/b66cf8e69499582a-s.woff2 +0 -0
- package/.next/static/media/d100b2a099e34044-s.woff2 +0 -0
- package/.next/static/media/f5271587012faf78-s.p.woff2 +0 -0
- package/.next/static/media/f639721981034f88-s.woff2 +0 -0
- package/.next/trace +3 -0
- package/.next/trace-build +1 -0
- package/.next/types/app/api/board/sync/route.ts +351 -0
- package/.next/types/app/api/config/route.ts +351 -0
- package/.next/types/app/api/documents/route.ts +351 -0
- package/.next/types/app/api/documents/sync/route.ts +351 -0
- package/.next/types/app/api/init/route.ts +351 -0
- package/.next/types/app/api/peers/route.ts +351 -0
- package/.next/types/app/api/transfer/download/route.ts +351 -0
- package/.next/types/app/api/transfer/prepare/route.ts +351 -0
- package/.next/types/app/api/transfer/request/route.ts +351 -0
- package/.next/types/app/api/transfer/shared/download/route.ts +351 -0
- package/.next/types/app/api/transfer/shared/route.ts +351 -0
- package/.next/types/app/api/transfer/start/route.ts +351 -0
- package/.next/types/app/api/transfer/tasks/route.ts +351 -0
- package/.next/types/app/layout.ts +87 -0
- package/.next/types/app/page.ts +87 -0
- package/.next/types/cache-life.d.ts +145 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +85 -0
- package/.next/types/validator.ts +187 -0
- package/README.md +85 -0
- package/bin/cli.js +51 -0
- package/package.json +52 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/services/configService.ts +144 -0
- package/src/services/documentService.ts +210 -0
- package/src/services/fileService.ts +595 -0
- package/src/services/mdnsService.ts +284 -0
- package/src/services/socketService.ts +214 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import { TransferTask } from '../types/transfer';
|
|
5
|
+
import { SocketService } from './socketService';
|
|
6
|
+
import { ConfigService } from './configService';
|
|
7
|
+
|
|
8
|
+
export interface FileMetadata {
|
|
9
|
+
taskId: string;
|
|
10
|
+
filePath: string;
|
|
11
|
+
fileName: string;
|
|
12
|
+
fileSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SharedFile {
|
|
16
|
+
id: string;
|
|
17
|
+
fileName: string;
|
|
18
|
+
fileSize: number;
|
|
19
|
+
uploadedAt: number;
|
|
20
|
+
deviceInfo: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FileService {
|
|
25
|
+
// 共享下载文件夹目录
|
|
26
|
+
private downloadsDir: string;
|
|
27
|
+
|
|
28
|
+
// 维护正在上传 (提供下载) 的文件映射: Key 为 taskId
|
|
29
|
+
private uploadTasks: Map<string, FileMetadata> = new Map();
|
|
30
|
+
|
|
31
|
+
// 维护正在下载的 HTTP 任务
|
|
32
|
+
private downloadTasks: Map<string, {
|
|
33
|
+
task: TransferTask;
|
|
34
|
+
request?: http.ClientRequest;
|
|
35
|
+
writer?: fs.WriteStream;
|
|
36
|
+
}> = new Map();
|
|
37
|
+
|
|
38
|
+
private constructor() {
|
|
39
|
+
// 动态获取由 ConfigService 提供的存储路径
|
|
40
|
+
const configService = ConfigService.getInstance();
|
|
41
|
+
this.downloadsDir = configService.getStoragePath();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static getInstance(): FileService {
|
|
45
|
+
const globalSymbols = global as any;
|
|
46
|
+
// 热重载自愈:若全局单例残留了旧方法,强制清空以在下一句重新 new 挂载新定义!
|
|
47
|
+
if (globalSymbols.__file_service_instance__ && typeof globalSymbols.__file_service_instance__.registerTransferTask !== 'function') {
|
|
48
|
+
console.warn('[FileService] 检测到 Next.js 热重载全局残留旧版单例,正在强制清空并重新初始化新版单例...');
|
|
49
|
+
globalSymbols.__file_service_instance__ = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!globalSymbols.__file_service_instance__) {
|
|
53
|
+
globalSymbols.__file_service_instance__ = new FileService();
|
|
54
|
+
}
|
|
55
|
+
return globalSymbols.__file_service_instance__;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 注册一个发送文件的本地任务,生成临时可下载映射
|
|
60
|
+
*/
|
|
61
|
+
public registerUpload(taskId: string, filePath: string, fileName: string, fileSize: number): void {
|
|
62
|
+
this.uploadTasks.set(taskId, { taskId, filePath, fileName, fileSize });
|
|
63
|
+
console.log(`[FileService] 已注册本地上传映射: ${fileName} (${taskId})`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getUpload(taskId: string): FileMetadata | undefined {
|
|
67
|
+
return this.uploadTasks.get(taskId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 启动极速 HTTP 客户端,从对端 Peer 极速拉取文件,支持断点续传
|
|
72
|
+
*/
|
|
73
|
+
public startDownload(
|
|
74
|
+
taskId: string,
|
|
75
|
+
fileName: string,
|
|
76
|
+
fileSize: number,
|
|
77
|
+
downloadUrl: string,
|
|
78
|
+
peerId: string,
|
|
79
|
+
peerName: string
|
|
80
|
+
): void {
|
|
81
|
+
const currentStorageDir = ConfigService.getInstance().getStoragePath();
|
|
82
|
+
const savePath = path.join(currentStorageDir, fileName);
|
|
83
|
+
|
|
84
|
+
// 检查是否已有下载任务,支持从上次的断点处继续下载
|
|
85
|
+
let offset = 0;
|
|
86
|
+
if (fs.existsSync(savePath)) {
|
|
87
|
+
const stat = fs.statSync(savePath);
|
|
88
|
+
// 如果文件已经下载完成,直接标记成功
|
|
89
|
+
if (stat.size >= fileSize) {
|
|
90
|
+
console.log(`[FileService] 文件 ${fileName} 已经存在且大小匹配,下载完成。`);
|
|
91
|
+
this.notifyDownloadComplete(taskId, savePath);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
offset = stat.size; // 获取已下载的大小作为偏移量
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`[FileService] 启动高速流式下载: ${fileName}, 偏移: ${offset} B, 地址: ${downloadUrl}`);
|
|
98
|
+
|
|
99
|
+
const task: TransferTask = {
|
|
100
|
+
id: taskId,
|
|
101
|
+
fileName,
|
|
102
|
+
fileSize,
|
|
103
|
+
transferredBytes: offset,
|
|
104
|
+
progress: Math.round((offset / fileSize) * 100),
|
|
105
|
+
speed: 0,
|
|
106
|
+
type: 'receive',
|
|
107
|
+
status: 'transferring',
|
|
108
|
+
peerId,
|
|
109
|
+
peerName,
|
|
110
|
+
startedAt: Date.now()
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 创建写文件流 (r+ 或 a 模式追加写入)
|
|
114
|
+
const writer = fs.createWriteStream(savePath, {
|
|
115
|
+
flags: offset > 0 ? 'r+' : 'w',
|
|
116
|
+
start: offset
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 解析请求 URL
|
|
120
|
+
const url = new URL(downloadUrl);
|
|
121
|
+
const options: http.RequestOptions = {
|
|
122
|
+
hostname: url.hostname,
|
|
123
|
+
port: url.port || 80,
|
|
124
|
+
path: `${url.pathname}${url.search}`,
|
|
125
|
+
method: 'GET',
|
|
126
|
+
headers: {
|
|
127
|
+
// 使用 HTTP Range 请求实现断点续传
|
|
128
|
+
'Range': `bytes=${offset}-`
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let lastTime = Date.now();
|
|
133
|
+
let lastBytes = offset;
|
|
134
|
+
|
|
135
|
+
const req = http.get(options, (res) => {
|
|
136
|
+
if (res.statusCode !== 200 && res.statusCode !== 206) {
|
|
137
|
+
task.status = 'failed';
|
|
138
|
+
task.error = `HTTP 错误代码: ${res.statusCode}`;
|
|
139
|
+
this.broadcastProgress(task);
|
|
140
|
+
writer.end();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
res.on('data', (chunk) => {
|
|
145
|
+
writer.write(chunk);
|
|
146
|
+
task.transferredBytes += chunk.length;
|
|
147
|
+
|
|
148
|
+
// 限制进度广播频率,防止卡顿,每 200ms 计算一次速度
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const duration = (now - lastTime) / 1000;
|
|
151
|
+
if (duration >= 0.2) {
|
|
152
|
+
const bytesDiff = task.transferredBytes - lastBytes;
|
|
153
|
+
task.speed = Math.round(bytesDiff / duration);
|
|
154
|
+
task.progress = Math.round((task.transferredBytes / fileSize) * 100);
|
|
155
|
+
|
|
156
|
+
this.broadcastProgress(task);
|
|
157
|
+
|
|
158
|
+
lastTime = now;
|
|
159
|
+
lastBytes = task.transferredBytes;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
res.on('end', () => {
|
|
164
|
+
writer.end();
|
|
165
|
+
if (task.transferredBytes >= fileSize) {
|
|
166
|
+
task.status = 'completed';
|
|
167
|
+
task.speed = 0;
|
|
168
|
+
task.progress = 100;
|
|
169
|
+
console.log(`[FileService] 文件下载成功: ${fileName}`);
|
|
170
|
+
this.broadcastProgress(task);
|
|
171
|
+
} else {
|
|
172
|
+
task.status = 'paused';
|
|
173
|
+
task.speed = 0;
|
|
174
|
+
console.log(`[FileService] 下载被意外中止: ${fileName}`);
|
|
175
|
+
this.broadcastProgress(task);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
req.on('error', (err) => {
|
|
181
|
+
console.error('[FileService] 下载请求发生网络异常:', err);
|
|
182
|
+
task.status = 'failed';
|
|
183
|
+
task.error = err.message;
|
|
184
|
+
this.broadcastProgress(task);
|
|
185
|
+
writer.end();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 缓存下载句柄以供取消/暂停使用
|
|
189
|
+
this.downloadTasks.set(taskId, { task, request: req, writer });
|
|
190
|
+
this.broadcastProgress(task);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 暂停或取消下载任务
|
|
195
|
+
*/
|
|
196
|
+
public pauseDownload(taskId: string): void {
|
|
197
|
+
const handle = this.downloadTasks.get(taskId);
|
|
198
|
+
if (handle) {
|
|
199
|
+
if (handle.request) {
|
|
200
|
+
handle.request.destroy();
|
|
201
|
+
}
|
|
202
|
+
if (handle.writer) {
|
|
203
|
+
handle.writer.end();
|
|
204
|
+
}
|
|
205
|
+
handle.task.status = 'paused';
|
|
206
|
+
handle.task.speed = 0;
|
|
207
|
+
this.broadcastProgress(handle.task);
|
|
208
|
+
console.log(`[FileService] 已暂停任务: ${handle.task.fileName}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 移除或删除下载任务记录
|
|
214
|
+
*/
|
|
215
|
+
public removeDownload(taskId: string): void {
|
|
216
|
+
this.pauseDownload(taskId);
|
|
217
|
+
this.downloadTasks.delete(taskId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 广播进度给前端
|
|
222
|
+
*/
|
|
223
|
+
private broadcastProgress(task: TransferTask): void {
|
|
224
|
+
SocketService.getInstance().broadcast('transfer:progress', task);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private notifyDownloadComplete(taskId: string, filePath: string): void {
|
|
228
|
+
SocketService.getInstance().broadcast('transfer:complete', {
|
|
229
|
+
taskId,
|
|
230
|
+
filePath,
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public getDownloadsDir(): string {
|
|
236
|
+
return ConfigService.getInstance().getStoragePath();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 获取共享文件元数据文件路径
|
|
241
|
+
*/
|
|
242
|
+
private getSharedFilesPath(): string {
|
|
243
|
+
const storageDir = ConfigService.getInstance().getStoragePath();
|
|
244
|
+
return path.join(storageDir, 'shared_files.json');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 获取公共共享物理目录
|
|
249
|
+
*/
|
|
250
|
+
public getSharedDir(): string {
|
|
251
|
+
const storageDir = ConfigService.getInstance().getStoragePath();
|
|
252
|
+
const sharedDir = path.join(storageDir, 'shared');
|
|
253
|
+
if (!fs.existsSync(sharedDir)) {
|
|
254
|
+
fs.mkdirSync(sharedDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
return sharedDir;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 将共享文件列表元数据写入 JSON
|
|
261
|
+
*/
|
|
262
|
+
private writeSharedFilesMetadata(files: SharedFile[]): void {
|
|
263
|
+
const filePath = this.getSharedFilesPath();
|
|
264
|
+
try {
|
|
265
|
+
fs.writeFileSync(filePath, JSON.stringify(files, null, 2), 'utf-8');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error('[FileService] 写入共享文件元数据失败:', err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 获取所有注册的公共共享文件列表,并自动清洗无效的丢失文件
|
|
273
|
+
*/
|
|
274
|
+
public getSharedFiles(): SharedFile[] {
|
|
275
|
+
const filePath = this.getSharedFilesPath();
|
|
276
|
+
const sharedDir = this.getSharedDir();
|
|
277
|
+
|
|
278
|
+
// 1. 读取元数据索引文件中的共享文件
|
|
279
|
+
let indexedFiles: SharedFile[] = [];
|
|
280
|
+
if (fs.existsSync(filePath)) {
|
|
281
|
+
try {
|
|
282
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
283
|
+
indexedFiles = JSON.parse(data);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error('[FileService] 读取共享文件元数据失败,将重新构建:', err);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 2. 读取物理共享目录中的实际文件列表
|
|
290
|
+
let physicalFiles: string[] = [];
|
|
291
|
+
if (fs.existsSync(sharedDir)) {
|
|
292
|
+
try {
|
|
293
|
+
physicalFiles = fs.readdirSync(sharedDir);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error('[FileService] 读取物理共享目录失败:', err);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 3. 物理文件校验:保留索引中记录且在磁盘上真实存在的文件
|
|
300
|
+
const validIndexedFiles = indexedFiles.filter(f => {
|
|
301
|
+
try {
|
|
302
|
+
return fs.existsSync(f.filePath);
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// 3.5. 强力防重清洗:若多个索引对象指向同一物理文件路径,优先保留具有真实物理设备信息(非自愈导入)的那条
|
|
309
|
+
const uniqueFilesMap = new Map<string, SharedFile>();
|
|
310
|
+
validIndexedFiles.forEach(file => {
|
|
311
|
+
const resolvedPath = path.resolve(file.filePath);
|
|
312
|
+
const existing = uniqueFilesMap.get(resolvedPath);
|
|
313
|
+
if (!existing) {
|
|
314
|
+
uniqueFilesMap.set(resolvedPath, file);
|
|
315
|
+
} else {
|
|
316
|
+
if (existing.deviceInfo === '本地存储自愈导入' && file.deviceInfo !== '本地存储自愈导入') {
|
|
317
|
+
uniqueFilesMap.set(resolvedPath, file);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
const uniqueIndexedFiles = Array.from(uniqueFilesMap.values());
|
|
322
|
+
|
|
323
|
+
// 4. 物理文件自愈重建索引:如果有物理文件没有在 validIndexedFiles 中记录,则自动登记
|
|
324
|
+
let hasChanges = uniqueIndexedFiles.length !== indexedFiles.length;
|
|
325
|
+
const finalFiles = [...uniqueIndexedFiles];
|
|
326
|
+
|
|
327
|
+
physicalFiles.forEach(fileName => {
|
|
328
|
+
const fullPath = path.join(sharedDir, fileName);
|
|
329
|
+
try {
|
|
330
|
+
const stat = fs.statSync(fullPath);
|
|
331
|
+
if (stat.isFile()) {
|
|
332
|
+
// 检查该物理文件是否已经在索引中注册 (通过绝对路径或文件名比对)
|
|
333
|
+
const isRegistered = finalFiles.some(f => {
|
|
334
|
+
return path.resolve(f.filePath) === path.resolve(fullPath) || f.fileName === fileName;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// 增加 5 秒创建时间保护缓冲区,避免因大文件分片合并落盘瞬间尚未登记元数据而被误判为未登记的物理文件
|
|
338
|
+
const isRecent = (Date.now() - (stat.mtimeMs || stat.birthtimeMs || Date.now())) < 5000;
|
|
339
|
+
|
|
340
|
+
if (!isRegistered && !isRecent) {
|
|
341
|
+
// 自动补全登记元数据
|
|
342
|
+
const fileId = `shared_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
343
|
+
const newFile: SharedFile = {
|
|
344
|
+
id: fileId,
|
|
345
|
+
fileName,
|
|
346
|
+
fileSize: stat.size,
|
|
347
|
+
uploadedAt: stat.mtimeMs || stat.birthtimeMs || Date.now(),
|
|
348
|
+
deviceInfo: '本地存储自愈导入',
|
|
349
|
+
filePath: fullPath
|
|
350
|
+
};
|
|
351
|
+
finalFiles.push(newFile);
|
|
352
|
+
hasChanges = true;
|
|
353
|
+
console.log(`[FileService] [自愈] 检测到未索引的物理文件,已自动重建索引: ${fileName}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(`[FileService] 获取物理文件属性失败: ${fileName}`, err);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// 5. 如果有新增元数据或过期元数据被清洗,自动写回索引文件
|
|
362
|
+
if (hasChanges) {
|
|
363
|
+
this.writeSharedFilesMetadata(finalFiles);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 6. 按上传时间降序排序返回
|
|
367
|
+
return finalFiles.sort((a, b) => b.uploadedAt - a.uploadedAt);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 注册一个公共共享文件
|
|
372
|
+
*/
|
|
373
|
+
public registerSharedFile(id: string, fileName: string, fileSize: number, filePath: string, deviceInfo: string): void {
|
|
374
|
+
// 1. 先安全读取元数据索引文件,将当前新文件提前登记,解决物理合并落盘瞬时与 getSharedFiles() 产生的 Race Condition
|
|
375
|
+
let indexedFiles: SharedFile[] = [];
|
|
376
|
+
const metaPath = this.getSharedFilesPath();
|
|
377
|
+
if (fs.existsSync(metaPath)) {
|
|
378
|
+
try {
|
|
379
|
+
const data = fs.readFileSync(metaPath, 'utf-8');
|
|
380
|
+
indexedFiles = JSON.parse(data);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error('[FileService] 提前读取元数据失败:', err);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const newFile: SharedFile = {
|
|
387
|
+
id,
|
|
388
|
+
fileName,
|
|
389
|
+
fileSize,
|
|
390
|
+
uploadedAt: Date.now(),
|
|
391
|
+
deviceInfo,
|
|
392
|
+
filePath
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// 剔除相同 id
|
|
396
|
+
const filtered = indexedFiles.filter(f => f.id !== id);
|
|
397
|
+
filtered.push(newFile);
|
|
398
|
+
|
|
399
|
+
// 提前写回元数据,使随后的 getSharedFiles 物理文件自愈扫描中能够正确识别“已登记”
|
|
400
|
+
this.writeSharedFilesMetadata(filtered);
|
|
401
|
+
|
|
402
|
+
// 2. 然后,再调用包含自愈与物理清洗的 getSharedFiles 得到最新的干净列表
|
|
403
|
+
const cleanFiles = this.getSharedFiles();
|
|
404
|
+
|
|
405
|
+
console.log(`[FileService] 公共共享文件已保存并写入索引: ${fileName} (${id})`);
|
|
406
|
+
|
|
407
|
+
// 3. 广播事件通知局域网所有在线伙伴
|
|
408
|
+
SocketService.getInstance().broadcast('shared-files:update', cleanFiles);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 获取指定公共文件详情
|
|
413
|
+
*/
|
|
414
|
+
public getSharedFile(id: string): SharedFile | undefined {
|
|
415
|
+
const files = this.getSharedFiles();
|
|
416
|
+
return files.find(f => f.id === id);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 物理删除某个公共共享文件
|
|
421
|
+
*/
|
|
422
|
+
public deleteSharedFile(id: string): boolean {
|
|
423
|
+
const files = this.getSharedFiles();
|
|
424
|
+
const target = files.find(f => f.id === id);
|
|
425
|
+
if (!target) return false;
|
|
426
|
+
|
|
427
|
+
// 1. 从物理磁盘中删除
|
|
428
|
+
try {
|
|
429
|
+
if (fs.existsSync(target.filePath)) {
|
|
430
|
+
fs.unlinkSync(target.filePath);
|
|
431
|
+
console.log(`[FileService] 共享物理文件已从磁盘中彻底清除: ${target.filePath}`);
|
|
432
|
+
}
|
|
433
|
+
} catch (err) {
|
|
434
|
+
console.error(`[FileService] 物理删除共享文件失败: ${target.filePath}`, err);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. 清理元数据索引
|
|
438
|
+
const filtered = files.filter(f => f.id !== id);
|
|
439
|
+
this.writeSharedFilesMetadata(filtered);
|
|
440
|
+
|
|
441
|
+
// 3. 广播更新
|
|
442
|
+
SocketService.getInstance().broadcast('shared-files:update', filtered.sort((a, b) => b.uploadedAt - a.uploadedAt));
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 缓存互传任务索引文件路径
|
|
447
|
+
private getTransferTasksPath(): string {
|
|
448
|
+
const storageDir = ConfigService.getInstance().getStoragePath();
|
|
449
|
+
return path.join(storageDir, 'transfer_tasks.json');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 从物理磁盘读取互传任务列表
|
|
453
|
+
public getTransferTasks(): Record<string, any> {
|
|
454
|
+
const filePath = this.getTransferTasksPath();
|
|
455
|
+
if (!fs.existsSync(filePath)) {
|
|
456
|
+
return {};
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
460
|
+
return JSON.parse(data);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.error('[FileService] 读取互传任务列表失败:', err);
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 物理写入互传任务列表
|
|
468
|
+
private writeTransferTasks(tasks: Record<string, any>): void {
|
|
469
|
+
const filePath = this.getTransferTasksPath();
|
|
470
|
+
try {
|
|
471
|
+
fs.writeFileSync(filePath, JSON.stringify(tasks, null, 2), 'utf-8');
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error('[FileService] 写入互传任务列表失败:', err);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 注册或更新一个互传任务
|
|
478
|
+
public registerTransferTask(taskId: string, task: any): void {
|
|
479
|
+
const tasks = this.getTransferTasks();
|
|
480
|
+
tasks[taskId] = {
|
|
481
|
+
...tasks[taskId],
|
|
482
|
+
...task,
|
|
483
|
+
updatedAt: Date.now()
|
|
484
|
+
};
|
|
485
|
+
this.writeTransferTasks(tasks);
|
|
486
|
+
console.log(`[FileService] 已持久化注册/更新互传任务: ${task.fileName} (${taskId})`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 获取特定客户端的 pending 状态任务 (用于补发提醒/自愈)
|
|
490
|
+
public getPendingTasksForClient(clientId: string): any[] {
|
|
491
|
+
const tasks = this.getTransferTasks();
|
|
492
|
+
return Object.values(tasks).filter((t: any) => t.peerId === clientId && t.status === 'pending');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 清理互传物理大文件,并在任务清单中更新状态为已删除
|
|
496
|
+
public cleanupTransferFile(taskId: string): void {
|
|
497
|
+
const upload = this.uploadTasks.get(taskId);
|
|
498
|
+
const tasks = this.getTransferTasks();
|
|
499
|
+
|
|
500
|
+
if (upload) {
|
|
501
|
+
try {
|
|
502
|
+
if (fs.existsSync(upload.filePath)) {
|
|
503
|
+
fs.unlinkSync(upload.filePath);
|
|
504
|
+
console.log(`[FileService] [自愈] 接收端已下载完毕,已物理清理发送端暂存文件: ${upload.filePath}`);
|
|
505
|
+
}
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error(`[FileService] 物理清理暂存文件失败: ${upload.filePath}`, err);
|
|
508
|
+
}
|
|
509
|
+
this.uploadTasks.delete(taskId);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 更新持久化状态
|
|
513
|
+
if (tasks[taskId]) {
|
|
514
|
+
if (tasks[taskId].status !== 'rejected' && tasks[taskId].status !== 'failed') {
|
|
515
|
+
tasks[taskId].status = 'completed';
|
|
516
|
+
tasks[taskId].progress = 100;
|
|
517
|
+
this.writeTransferTasks(tasks);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 物理擦除任何互传暂存文件,并更新指定任务状态 (用于拒绝或取消)
|
|
523
|
+
public cancelAndCleanupTransfer(taskId: string, finalStatus: 'failed' | 'rejected'): void {
|
|
524
|
+
const tasks = this.getTransferTasks();
|
|
525
|
+
const upload = this.uploadTasks.get(taskId);
|
|
526
|
+
|
|
527
|
+
// 1. 双重物理文件擦除
|
|
528
|
+
if (upload) {
|
|
529
|
+
try {
|
|
530
|
+
if (fs.existsSync(upload.filePath)) {
|
|
531
|
+
fs.unlinkSync(upload.filePath);
|
|
532
|
+
console.log(`[FileService] [取消/拒绝] 已物理清理发送端暂存文件: ${upload.filePath}`);
|
|
533
|
+
}
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(`[FileService] [取消/拒绝] 物理清理暂存文件失败: ${upload.filePath}`, err);
|
|
536
|
+
}
|
|
537
|
+
this.uploadTasks.delete(taskId);
|
|
538
|
+
} else {
|
|
539
|
+
// 兜底路径计算强行 unlink
|
|
540
|
+
const cacheDir = path.join(process.cwd(), 'upload_cache');
|
|
541
|
+
const possiblePath = path.join(cacheDir, taskId);
|
|
542
|
+
try {
|
|
543
|
+
if (fs.existsSync(possiblePath)) {
|
|
544
|
+
fs.unlinkSync(possiblePath);
|
|
545
|
+
console.log(`[FileService] [取消/拒绝/兜底] 已通过路径物理清理暂存文件: ${possiblePath}`);
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
console.error(`[FileService] [取消/拒绝/兜底] 物理清理暂存文件失败: ${possiblePath}`, err);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 2. 更新任务状态持久化落盘
|
|
553
|
+
if (tasks[taskId]) {
|
|
554
|
+
tasks[taskId].status = finalStatus;
|
|
555
|
+
tasks[taskId].progress = 0;
|
|
556
|
+
this.writeTransferTasks(tasks);
|
|
557
|
+
console.log(`[FileService] 任务 ${taskId} 状态已更新为 ${finalStatus} 并成功落盘`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* 物理删除指定的互传任务记录
|
|
563
|
+
*/
|
|
564
|
+
public deleteTransferTask(taskId: string): void {
|
|
565
|
+
const tasks = this.getTransferTasks();
|
|
566
|
+
if (tasks[taskId]) {
|
|
567
|
+
delete tasks[taskId];
|
|
568
|
+
this.writeTransferTasks(tasks);
|
|
569
|
+
console.log(`[FileService] 已物理删除互传任务记录: ${taskId}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 一键清空该客户端所有已完结的互传任务记录
|
|
575
|
+
*/
|
|
576
|
+
public clearHistoryTransferTasks(clientId: string): void {
|
|
577
|
+
const tasks = this.getTransferTasks();
|
|
578
|
+
let hasChanges = false;
|
|
579
|
+
Object.keys(tasks).forEach(taskId => {
|
|
580
|
+
const t = tasks[taskId];
|
|
581
|
+
if (
|
|
582
|
+
t &&
|
|
583
|
+
(t.senderId === clientId || t.peerId === clientId || t.receiverId === clientId) &&
|
|
584
|
+
(t.status === 'completed' || t.status === 'failed' || t.status === 'rejected')
|
|
585
|
+
) {
|
|
586
|
+
delete tasks[taskId];
|
|
587
|
+
hasChanges = true;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
if (hasChanges) {
|
|
591
|
+
this.writeTransferTasks(tasks);
|
|
592
|
+
console.log(`[FileService] 已清空客户端 ${clientId} 所有完结的物理互传任务记录`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|