browserFiles.ts 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. import {createHash} from 'node:crypto'
  2. import {constants} from 'node:fs'
  3. import {open} from 'node:fs/promises'
  4. import {posix} from 'node:path'
  5. import {lookup} from 'mime-types'
  6. import type {BrowserUpload} from '../runner/browser/types.js'
  7. import type {WorkspaceStore} from './workspaceStore.js'
  8. import type {WorkspaceNodes} from './workspaceGateway.js'
  9. import type {UploadStore} from './uploads.js'
  10. import type {StoredWorkspaceFile} from './workspaceAssets.js'
  11. import {FILE_TRANSFER_CHUNK_BYTES,type FileTransferEndpoint} from '../shared/fileTransfer.js'
  12. import {copyBetweenEndpoints} from './fileTransfer.js'
  13. import {HttpError} from './errors.js'
  14. export async function browserOwnedFile(store:WorkspaceStore,nodes:WorkspaceNodes,uploads:UploadStore,owner:string,id:string):Promise<BrowserUpload>{
  15. const file=store.visibleFiles(owner).find(file=>file.id===id)??uploads.records([id],owner)[0]!
  16. if('sourceNodeId' in file&&(file as StoredWorkspaceFile).sourceNodeId&&'profile' in file)nodes.requireSource(owner,{nodeId:(file as StoredWorkspaceFile).sourceNodeId!,profile:String(file.profile)})
  17. const handle=await open(file.path,constants.O_RDONLY|constants.O_NOFOLLOW)
  18. try{
  19. const size=(await handle.stat()).size
  20. if(size>25*1024*1024)throw new HttpError(413,'上传文件不能超过25 MiB','browser_upload_limit')
  21. const buffer=Buffer.alloc(size+1);let offset=0
  22. while(offset<buffer.length){const {bytesRead}=await handle.read(buffer,offset,buffer.length-offset,offset);if(!bytesRead)break;offset+=bytesRead}
  23. if(offset!==size)throw new HttpError(409,'文件读取期间发生变化','browser_upload_changed')
  24. return {name:file.name,mimeType:file.mimeType,buffer:buffer.subarray(0,size)}
  25. }finally{await handle.close()}
  26. }
  27. /** Reuse the same checked, chunked transfer as desktop/VM copies. */
  28. export async function browserVmFile(transfer:(action:Record<string,unknown>)=>Promise<any>,path:string,check:()=>void,file?:BrowserUpload,overwrite=false,maxBytes=25*1024*1024):Promise<BrowserUpload|unknown>{
  29. if(!posix.isAbsolute(path)||path.includes('\0'))throw new HttpError(400,'请选择虚拟机内的绝对路径','browser_vm_path_invalid')
  30. let bytes=file?.buffer??Buffer.alloc(0),expected=0,expectedHash='',received=0,complete=false
  31. const chunks:Buffer[]=[]
  32. const memory:FileTransferEndpoint={host:'browser',name:'托管浏览器',path:'/transfer',chunks:true,call:async action=>{
  33. check()
  34. if(action.op==='transfer-read-open')return {size:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex')}
  35. if(action.op==='transfer-read'){const offset=Number(action.offset);return {offset,data:bytes.subarray(offset,offset+FILE_TRANSFER_CHUNK_BYTES).toString('base64')}}
  36. if(action.op==='transfer-write-open'){expected=Number(action.size);expectedHash=String(action.sha256);if(!Number.isSafeInteger(expected)||expected<0||expected>maxBytes)throw new HttpError(413,'文件超过传输上限','browser_file_limit');return {ok:true}}
  37. if(action.op==='transfer-append'){const chunk=Buffer.from(String(action.data),'base64');if(action.offset!==received||received+chunk.length>expected)throw new HttpError(502,'文件分块无效','browser_file_invalid');chunks.push(chunk);received+=chunk.length;return {received}}
  38. if(action.op==='transfer-finish'){
  39. bytes=Buffer.concat(chunks)
  40. if(bytes.length!==expected||createHash('sha256').update(bytes).digest('hex')!==expectedHash)throw new HttpError(502,'文件校验失败','browser_file_invalid')
  41. complete=true;return {complete:true,path:'/transfer',size:expected,sha256:expectedHash}
  42. }
  43. if(action.op==='transfer-status')return {complete,path:'/transfer',size:expected,sha256:expectedHash}
  44. if(action.op==='transfer-abort')return {ok:true}
  45. throw new HttpError(400,'文件操作无效','browser_file_invalid')
  46. }}
  47. const vm:FileTransferEndpoint={host:'vm',name:'虚拟机',path,chunks:true,call:transfer}
  48. const result=await copyBetweenEndpoints(file?memory:vm,file?vm:memory,maxBytes,overwrite,check)
  49. return file?result:{name:posix.basename(path),mimeType:lookup(path)||'application/octet-stream',buffer:bytes}
  50. }