1+ import path from 'path'
2+ import fs from 'fs'
3+ import { Files } from '../../file/main'
4+ import { PluginContext } from '../type'
5+
6+ type AssetProgress = {
7+ total : number
8+ completed : number
9+ percent : number
10+ speed : number
11+ status : 'downloading' | 'completed' | 'failed'
12+ error ?: string
13+ }
14+
15+ const downloadProgressMap = new Map < string , AssetProgress > ( )
16+
17+ const getKey = ( pluginName : string , pluginFilePath : string ) => `${ pluginName } ::${ pluginFilePath } `
18+
19+ const getPluginRoot = ( context : PluginContext ) : string | null => {
20+ if ( context . _plugin . runtime && context . _plugin . runtime . root ) {
21+ return context . _plugin . runtime . root
22+ }
23+ return null
24+ }
25+
26+ export const assetDownload = async ( context : PluginContext , data : any ) : Promise < void > => {
27+ const { url, pluginFilePath, options } = data
28+ const timeout = ( options ?. timeout ?? 3600 ) * 1000
29+
30+ const pluginRoot = getPluginRoot ( context )
31+ if ( ! pluginRoot ) {
32+ throw new Error ( 'Plugin root not found' )
33+ }
34+
35+ const key = getKey ( context . _plugin . name , pluginFilePath )
36+ const targetPath = path . join ( pluginRoot , pluginFilePath )
37+ const targetDir = path . dirname ( targetPath )
38+
39+ // Ensure target directory exists
40+ if ( ! fs . existsSync ( targetDir ) ) {
41+ fs . mkdirSync ( targetDir , { recursive : true } )
42+ }
43+
44+ // Download to temp file first, then move to final location
45+ const ext = path . extname ( pluginFilePath ) || 'tmp'
46+ const tempFile = await Files . temp ( ext , 'asset-download' )
47+
48+ const progress : AssetProgress = {
49+ total : 0 ,
50+ completed : 0 ,
51+ percent : 0 ,
52+ speed : 0 ,
53+ status : 'downloading' ,
54+ }
55+ downloadProgressMap . set ( key , progress )
56+
57+ const controller = new AbortController ( )
58+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeout )
59+
60+ try {
61+ const res = await fetch ( url , {
62+ method : 'GET' ,
63+ headers : { 'User-Agent' : 'FocusAny' } ,
64+ signal : controller . signal ,
65+ } )
66+
67+ if ( ! res . ok ) {
68+ throw new Error ( `DownloadError:${ url } ` )
69+ }
70+
71+ const contentLength = res . headers . get ( 'content-length' )
72+ const totalSize = contentLength ? parseInt ( contentLength , 10 ) : 0
73+ progress . total = totalSize
74+
75+ const reader = res . body ! . getReader ( )
76+ const fileStream = fs . createWriteStream ( tempFile )
77+
78+ let lastCompleted = 0
79+ let lastTime = Date . now ( )
80+
81+ const pump = async ( ) => {
82+ while ( true ) {
83+ const { done, value } = await reader . read ( )
84+ if ( done ) break
85+
86+ fileStream . write ( value )
87+
88+ const now = Date . now ( )
89+ progress . completed += value . length
90+ if ( totalSize > 0 ) {
91+ progress . percent = Math . round ( ( progress . completed / totalSize ) * 100 )
92+ }
93+ const elapsed = ( now - lastTime ) / 1000
94+ if ( elapsed > 0.1 ) {
95+ progress . speed = Math . round ( ( progress . completed - lastCompleted ) / elapsed )
96+ lastCompleted = progress . completed
97+ lastTime = now
98+ }
99+ }
100+ }
101+
102+ await pump ( )
103+ clearTimeout ( timeoutId )
104+
105+ await new Promise < void > ( ( resolve , reject ) => {
106+ fileStream . end ( ( err ) => {
107+ if ( err ) {
108+ reject ( err )
109+ return
110+ }
111+ // Move temp file to final plugin path
112+ if ( fs . existsSync ( targetPath ) ) {
113+ fs . unlinkSync ( targetPath )
114+ }
115+ fs . renameSync ( tempFile , targetPath )
116+
117+ progress . status = 'completed'
118+ progress . percent = 100
119+ progress . completed = progress . total
120+ resolve ( )
121+ } )
122+ } )
123+ } catch ( e : any ) {
124+ clearTimeout ( timeoutId )
125+ progress . status = 'failed'
126+ progress . error = e . message || '' + e
127+
128+ // Clean up temp file on failure
129+ if ( fs . existsSync ( tempFile ) ) {
130+ try {
131+ fs . unlinkSync ( tempFile )
132+ } catch ( _ ) {
133+ // ignore cleanup error
134+ }
135+ }
136+
137+ if ( e . name === 'AbortError' ) {
138+ throw new Error ( 'Download timeout' )
139+ }
140+ throw e
141+ }
142+ }
143+
144+ export const assetExists = async ( context : PluginContext , data : any ) : Promise < boolean > => {
145+ const { pluginFilePath } = data
146+ const pluginRoot = getPluginRoot ( context )
147+ if ( ! pluginRoot ) return false
148+
149+ const targetPath = path . join ( pluginRoot , pluginFilePath )
150+ return await Files . exists ( targetPath , { isDataPath : false } )
151+ }
152+
153+ export const assetDelete = async ( context : PluginContext , data : any ) : Promise < void > => {
154+ const { pluginFilePath } = data
155+ const pluginRoot = getPluginRoot ( context )
156+ if ( ! pluginRoot ) return
157+
158+ const targetPath = path . join ( pluginRoot , pluginFilePath )
159+ await Files . deletes ( targetPath , { isDataPath : false } )
160+
161+ // Clear progress tracking
162+ const key = getKey ( context . _plugin . name , pluginFilePath )
163+ downloadProgressMap . delete ( key )
164+ }
165+
166+ export const assetProgress = async ( context : PluginContext , data : any ) : Promise < AssetProgress | null > => {
167+ const { pluginFilePath } = data
168+ const key = getKey ( context . _plugin . name , pluginFilePath )
169+ const progress = downloadProgressMap . get ( key )
170+ if ( ! progress ) return null
171+
172+ return {
173+ total : progress . total ,
174+ completed : progress . completed ,
175+ percent : progress . percent ,
176+ speed : progress . speed ,
177+ status : progress . status ,
178+ error : progress . error ,
179+ }
180+ }
0 commit comments