stashr Uploader
Upload files to stashr.wtf programmatically from Node.js. Supports auto-chunked upload (>17MB), expiry, download limit, password, and generates valid share URLs — no API key needed.
28 Mei 2026271 file
stashr-sdk.jsjavascript · 294 baris
// stashr-sdk.js — Unofficial Node.js SDK for stashr.wtf// npm install lz-string form-data node-fetchconst fs = require('fs');const path = require('path');const FormData = require('form-data');const fetch = require('node-fetch');const LZString = require('lz-string');const STASHR_API = { upload: 'https://upload.stashr.wtf', shortener: 'https://s.stashr.wtf', downloads: 'https://downloads.stashr.wtf', report: 'https://api.stashr.wtf',};const CHUNK_SIZE = 17 * 1024 * 1024;const SPLIT_THRESHOLD = 17 * 1024 * 1024;const MAX_FILE_SIZE = 500 * 1024 * 1024;const PARALLEL_CHUNKS = 5;const MAX_RETRIES = 2;const EXPIRY = { '1h':1,'6h':6,'12h':12,'1d':24,'3d':72,'7d':168,'30d':720,never:'never',};function generateFileId() { return `file_${Date.now().toString(36)}_${Math.random().toString(36).substring(2,11)}`;}function generateMasterId() { return `chunked_${Date.now()}_${Math.random().toString(36).substr(2,9)}`;}function formatFileSize(bytes) { if (!bytes) return '0 B'; const k=1024, sizes=['B','KB','MB','GB']; const i=Math.floor(Math.log(bytes)/Math.log(k)); return Math.round(bytes/Math.pow(k,i)*100)/100+' '+sizes[i];}function sleep(ms) { return new Promise(r=>setTimeout(r,ms)); }function guessMime(fileName) { const ext=path.extname(fileName).toLowerCase(); const map={ '.jpg':'image/jpeg','.jpeg':'image/jpeg','.png':'image/png', '.gif':'image/gif','.webp':'image/webp','.mp4':'video/mp4', '.mp3':'audio/mpeg','.pdf':'application/pdf', '.zip':'application/zip','.txt':'text/plain', '.json':'application/json','.html':'text/html', }; return map[ext]||'application/octet-stream';}function compressFileId(fileId) { return Buffer.from(fileId).toString('base64') .replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');}function encodeFlags(hasPassword,hasExpiry,hasLimit,hasNote,hasCreated,isChunked) { let f=0; if(hasPassword) f|=1; if(hasExpiry) f|=2; if(hasLimit) f|=4; if(hasNote) f|=8; if(hasCreated) f|=16; if(isChunked) f|=32; return f.toString(36);}function buildShareUrl(fileData) { let compressedId; if(fileData.chunked && fileData.file_ids) { if(fileData.bot_indices && fileData.bot_indices.length===fileData.file_ids.length) { compressedId='CHUNKED2:'+fileData.file_ids.map((id,i)=>`${fileData.bot_indices[i]}:${compressFileId(id)}`).join(','); } else { compressedId='CHUNKED:'+fileData.file_ids.map(id=>compressFileId(id)).join(','); } } else { compressedId=compressFileId(fileData.file_id); } let filename=fileData.file_name; let isNameCompressed=false; if(filename.length>20) { filename=LZString.compressToEncodedURIComponent(filename); isNameCompressed=true; } const flags=encodeFlags(!!fileData.password,!!fileData.expiry,!!fileData.downloadLimit,!!fileData.note,!!fileData.uploadedAt,!!fileData.chunked); const parts=[flags,isNameCompressed?'1':'0',compressedId,filename]; if(fileData.password) parts.push('1'); if(fileData.expiry) parts.push(Math.floor(new Date(fileData.expiry).getTime()/1000).toString(36)); if(fileData.downloadLimit) parts.push(fileData.downloadLimit.toString(36)); if(fileData.note) parts.push(fileData.note); if(fileData.uploadedAt) parts.push(Math.floor(new Date(fileData.uploadedAt).getTime()/1000).toString(36)); if(fileData.chunked) { parts.push(fileData.file_size.toString(36)); if(fileData.master_file_id) parts.push(fileData.master_file_id); } if(fileData.tracking_id) parts.push(fileData.tracking_id); const encoded=LZString.compressToEncodedURIComponent(parts.join('|')); return `https://stashr.wtf/?d=${encoded}`;}class StashrError extends Error { constructor(message,code) { super(message); this.name='StashrError'; this.code=code||'UNKNOWN'; }}class Stashr { constructor(options={}) { this._debug=options.debug||false; } _log(...a) { if(this._debug) console.log('[stashr]',...a); } _warn(...a) { if(this._debug) console.warn('[stashr]',...a); } async upload(source, opts={}) { let buffer, fileName, mimeType; if(typeof source==='string') { buffer=fs.readFileSync(source); fileName=opts.fileName||path.basename(source); mimeType=opts.mimeType||guessMime(fileName); } else if(Buffer.isBuffer(source)||source instanceof Uint8Array) { buffer=Buffer.isBuffer(source)?source:Buffer.from(source); fileName=opts.fileName; mimeType=opts.mimeType||'application/octet-stream'; if(!fileName) throw new StashrError('fileName required for Buffer upload','MISSING_FILENAME'); } else { throw new StashrError('source must be file path or Buffer','INVALID_SOURCE'); } if(buffer.length>MAX_FILE_SIZE) throw new StashrError(`File too large (max ${formatFileSize(MAX_FILE_SIZE)})`,'FILE_TOO_LARGE'); const expiry = opts.expiry??'never'; const downloadLimit = opts.downloadLimit??'unlimited'; const password = opts.password||null; const note = opts.note?String(opts.note).slice(0,50):null; const progress = opts.onProgress||(()=>{}); const expiryHours = expiry==='never'?null:parseInt(expiry); const expiryTimestamp = expiryHours?Date.now()+expiryHours*3600000:null; const limitNum = downloadLimit==='unlimited'?null:parseInt(downloadLimit); const uploadedAt = new Date().toISOString(); const metadata = { fileName, fileSize:formatFileSize(buffer.length), fileSizeBytes:buffer.length, fileType:mimeType, password, expiry:expiry==='never'?'Never':`${expiryHours}h`, expiryTimestamp, downloadLimit:limitNum, note, userID:'SDK_USER', uploadCount:1, uploadTime:new Date().toLocaleString(), shareLink:'', isLoggedIn:false, accountUsername:null, isEncrypted:false, }; const isChunked = buffer.length>SPLIT_THRESHOLD; const masterFileId = generateMasterId(); let fileId, filePath, fileIds=[], botIndices=[]; if(isChunked) { progress(5,'Preparing chunked upload...'); ({fileIds,botIndices}=await this._uploadChunked(buffer,fileName,mimeType,masterFileId,metadata,password,progress)); } else { progress(5,'Uploading...'); const result=await this._uploadSingle(buffer,fileName,mimeType,metadata,password); fileId=result.file_id; filePath=result.file_path||null; progress(90,'Upload complete'); } const trackingId=generateFileId(); try { await fetch(`${STASHR_API.downloads}/register`,{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ fileId:trackingId, fileName, downloadLimit:limitNum, backendFileId:isChunked?fileIds[0]:fileId, backendFileIds:isChunked?fileIds:null, chunked:isChunked, expiresAt:expiryTimestamp?new Date(expiryTimestamp).toISOString():null, }), }); this._log('Tracking registered:',trackingId); } catch(e) { this._warn('Tracker failed:',e.message); } const shareUrl = buildShareUrl({ tracking_id:trackingId, file_id:isChunked?fileIds[0]:fileId, file_ids:isChunked?fileIds:null, bot_indices:isChunked?botIndices:null, chunked:isChunked, master_file_id:isChunked?masterFileId:null, file_name:fileName, file_size:buffer.length, password, expiry:expiryTimestamp?new Date(expiryTimestamp).toISOString():null, downloadLimit:limitNum, note, uploadedAt, }); progress(100,'Done'); return { ok:true, trackingId, fileId:isChunked?fileIds[0]:fileId, fileIds:isChunked?fileIds:null, chunked:isChunked, masterFileId:isChunked?masterFileId:null, filePath:isChunked?null:filePath, fileName, fileSize:buffer.length, expiry:expiryTimestamp?new Date(expiryTimestamp).toISOString():null, downloadLimit:limitNum, hasPassword:!!password, note, shareUrl, }; } async _uploadSingle(buffer, fileName, mimeType, metadata, password) { const form = new FormData(); form.append('document', buffer, { filename:fileName, contentType:mimeType }); form.append('metadata', JSON.stringify(metadata)); form.append('password', password||''); const res = await fetch(`${STASHR_API.upload}/upload`,{ method:'POST', body:form, headers:form.getHeaders() }); const data = await res.json(); if(!res.ok||!data?.result?.document?.file_id) throw new StashrError(data?.description||data?.error||`Server error ${res.status}`,'UPLOAD_FAILED'); if(!data.ok) throw new StashrError(data.description||'Upload failed','UPLOAD_FAILED'); return data.result.document; } async _uploadChunked(buffer, fileName, mimeType, masterFileId, metadata, password, progress) { const totalChunks=Math.ceil(buffer.length/CHUNK_SIZE); this._log(`Chunked: ${totalChunks} chunks`); let preflightSessionId=null; try { const pfRes=await fetch(`${STASHR_API.upload}/upload/preflight`,{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({fileName,fileSize:buffer.length,totalChunks}), }); const pfData=await pfRes.json(); if(pfData.ok) preflightSessionId=pfData.session_id; else throw new StashrError(pfData.description||'Pre-flight failed','PREFLIGHT_FAILED'); } catch(e) { if(e instanceof StashrError) throw e; this._warn('Pre-flight skipped:',e.message); } const uploadedChunks=new Array(totalChunks).fill(null); const uploadedBotIndices=new Array(totalChunks).fill(0); let completedCount=0; const uploadChunk=async(idx,retry=0)=>{ const start=idx*CHUNK_SIZE, end=Math.min(start+CHUNK_SIZE,buffer.length); const chunk=buffer.slice(start,end); const form=new FormData(); form.append('document',chunk,{filename:`${fileName}.part${idx+1}of${totalChunks}`,contentType:mimeType}); form.append('chunk_index',String(idx)); form.append('total_chunks',String(totalChunks)); form.append('master_file_id',masterFileId); if(idx===0) { form.append('metadata',JSON.stringify(metadata)); form.append('password',password||''); if(preflightSessionId) form.append('preflight_session',preflightSessionId); } try { const res=await fetch(`${STASHR_API.upload}/upload/v2`,{method:'POST',body:form,headers:form.getHeaders()}); const data=await res.json(); if(!data?.result?.document?.file_id) throw new StashrError(`Chunk ${idx+1}: invalid response`,'CHUNK_FAILED'); if(!data.ok) throw new StashrError(data.description||`Chunk ${idx+1} failed`,'CHUNK_FAILED'); uploadedChunks[idx]=data.result.document.file_id; uploadedBotIndices[idx]=data.bot_index??0; completedCount++; progress(10+Math.round((completedCount/totalChunks)*80),`Uploaded ${completedCount}/${totalChunks} chunks`); } catch(err) { if((err.name==='AbortError'||err.message?.includes('network'))&&retry<MAX_RETRIES) { await sleep(1000*(retry+1)); return uploadChunk(idx,retry+1); } throw err; } }; let nextIdx=0, error=null; const worker=async()=>{ while(nextIdx<totalChunks&&!error){ const i=nextIdx++; try{await uploadChunk(i);}catch(e){error=e;throw e;} } }; await Promise.all(Array.from({length:Math.min(PARALLEL_CHUNKS,totalChunks)},worker)); if(error) throw error; return {fileIds:uploadedChunks,botIndices:uploadedBotIndices}; } async shorten(url,opts={}) { const body={url}; if(opts.fileName) body.fileName=opts.fileName; if(opts.fileId) body.fileId=opts.fileId; const res=await fetch(`${STASHR_API.shortener}/api/shorten`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); const data=await res.json(); return data.success&&data.shortUrl?data.shortUrl:null; } async getFile(fileId) { const res=await fetch(`${STASHR_API.upload}/getFile`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId})}); const data=await res.json(); if(!data.ok) throw new StashrError(data.description||'getFile failed','GET_FILE_FAILED'); return data.result; } async stats() { const res=await fetch(`${STASHR_API.upload}/stats`); const data=await res.json(); if(!data.ok) throw new StashrError('Failed to fetch stats','STATS_FAILED'); return data.stats; } async trackerStats(trackingId) { const res=await fetch(`${STASHR_API.downloads}/stats/${trackingId}`); const data=await res.json(); if(!data.ok) throw new StashrError('Tracker stats failed','TRACKER_FAILED'); return {downloads:data.downloads||0,limit:data.limit??null}; } async report({fileId,reason}) { if(!fileId||!reason) throw new StashrError('fileId and reason required','INVALID_ARGS'); const res=await fetch(`${STASHR_API.report}/report`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId,reason})}); return res.json(); }}module.exports = { Stashr, StashrError, EXPIRY };Komentar
Masuk untuk ikut berkomentar.
Belum ada komentar.