Loading snippets...
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.
// stashr-sdk.js — Unofficial Node.js SDK for stashr.wtf
// npm install lz-string form-data node-fetch
const 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 };