1window.uploader = (() => { 2 const API_URL = '/api/v2'; 3 const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB minimum for R2 multipart 4 const DEFAULT_CONCURRENCY = 4; 5 const MAX_CHUNK_SIZE = 10 * 1024 * 1024; 6 const MIN_CONCURRENCY = 2; 7 const MAX_CONCURRENCY = 2; 8 9 async function getApiToken() { 10 if (!api.apitoken) { 11 return await api.authorize(hd.sessionkey()); 12 } 13 return api.apitoken; 14 } 15 16 function getAdaptiveSettings() { 17 const storedSpeed = parseFloat(localStorage.getItem('uploadSpeed') || '0') || 0; 18 const chunkSize = Math.max(DEFAULT_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, storedSpeed * 5 * 1024 * 1024)); 19 const concurrency = Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(storedSpeed / 2))); 20 return { chunkSize, concurrency: storedSpeed > 0 ? concurrency : DEFAULT_CONCURRENCY }; 21 } 22 23 async function getJwtToken(params) { 24 const token = await getApiToken(); 25 const response = await fetch(`${API_URL}/Upload/Token`, { 26 method: 'POST', 27 headers: { 28 'Authorization': `Bearer ${token}`, 29 'Content-Type': 'application/json', 30 }, 31 body: JSON.stringify(params), 32 }); 33 if (!response.ok) { 34 throw new Error(await response.text() || `Failed to get JWT: ${response.status}`); 35 } 36 const data = await response.json(); 37 return data.jwt; 38 } 39 40 function getWorkerUrl() { 41 //return 'https://uploadbucket.hd.pics'; 42 return window.hd.isDev ? 'https://uploadbucket-dev.hd.pics' : 'https://uploadbucket.hd.pics'; 43 }; 44 45 // Custom Promise Queue for concurrency 46 class PromiseQueue { 47 constructor(concurrency = 4) { 48 this.concurrency = concurrency; 49 this.queue = []; 50 this.active = 0; 51 } 52 53 add(task) { 54 return new Promise((resolve, reject) => { 55 this.queue.push(async () => { 56 try { 57 const result = await task(); 58 resolve(result); 59 } catch (err) { 60 reject(err); 61 } finally { 62 this.active--; 63 this.next(); 64 } 65 }); 66 this.next(); 67 }); 68 } 69 70 next() { 71 if (this.active >= this.concurrency || this.queue.length === 0) return; 72 this.active++; 73 const task = this.queue.shift(); 74 task(); 75 } 76 77 onIdle() { 78 return Promise.allSettled(this.queue.map(() => new Promise(resolve => setTimeout(resolve, 0)))); 79 } 80 } 81 82 // Retry utility with exponential backoff 83 async function retryFetch(url, options, maxRetries = 3, baseDelay = 1000) { 84 for (let attempt = 1; attempt <= maxRetries; attempt++) { 85 try { 86 const res = await fetch(url, options); 87 if (res.ok) { 88 return res; 89 } 90 throw new Error(`Fetch failed with status ${res.status}`); 91 } catch (err) { 92 if (attempt === maxRetries) { 93 throw err; 94 } 95 const delay = baseDelay * Math.pow(2, attempt - 1); 96 console.warn(`Retry ${attempt}/${maxRetries} for ${url}: ${err.message}. Retrying after ${delay}ms`); 97 await new Promise(resolve => setTimeout(resolve, delay)); 98 } 99 } 100 } 101 102 // CHUNK PUT UPLOAD 103 async function uploadFile(file, params = {}, success = () => {}, progress = () => {}, error = () => {}) { 104 // Use single-part upload for small files (< 5MB) 105 if (file.size < DEFAULT_CHUNK_SIZE) { 106 try { 107 const response = await uploadFileSimple(file, params); 108 if (response.mediaId) { 109 success(response); 110 } else { 111 console.error('Single-part upload failed:', response); 112 error(new Error('Single-part upload failed'), response); 113 } 114 return; 115 } catch (e) { 116 error(e); 117 return; 118 } 119 } 120 121 // Ensure consistent chunk size for multipart 122 const { chunkSize: rawChunkSize, concurrency } = getAdaptiveSettings(); 123 const chunkSize = Math.max(DEFAULT_CHUNK_SIZE, Math.round(rawChunkSize / DEFAULT_CHUNK_SIZE) * DEFAULT_CHUNK_SIZE); 124 const numChunks = Math.ceil(file.size / chunkSize); 125 const speeds = []; 126 127 // Validate chunk sizes 128 for (let i = 0; i < numChunks - 1; i++) { 129 const start = i * chunkSize; 130 const end = Math.min(start + chunkSize, file.size); 131 if (end - start < DEFAULT_CHUNK_SIZE && i < numChunks - 1) { 132 error(new Error('Non-trailing chunk too small')); 133 return; 134 } 135 } 136 137 // Get file info 138 if (!params.Name) { params.Name = file.name.split('.')[0]; } 139 if (!params.Extension) { params.Extension = (file.name.split('.').pop() || '').replace('jpeg', 'jpg'); } 140 141 // Get JWT from API 142 try { 143 const jwt = await getJwtToken(params); 144 145 // Get presigned URLs 146 const presignResponse = await fetch(`${getWorkerUrl()}/presigns`, { 147 method: 'POST', 148 headers: { 149 'Authorization': `Bearer ${jwt}`, 150 'Content-Type': 'application/json', 151 }, 152 body: JSON.stringify({ contentType: file.type, numParts: numChunks }), 153 }); 154 if (!presignResponse.ok) { 155 error(new Error(await presignResponse.text() || `Presign failed: ${presignResponse.status}`)); 156 return; 157 } 158 const { uploadId, urls } = await presignResponse.json(); 159 160 // Upload chunks with custom queue 161 const queue = new PromiseQueue(concurrency); 162 const parts = []; 163 const uploadPromises = []; 164 let hasError = false;
165 166 for (let i = 0; i < numChunks; i++) { 167 uploadPromises.push( 168 queue.add(async () => { 169 if (hasError) return; 170 const start = i * chunkSize; 171 const end = Math.min(start + chunkSize, file.size); 172 const chunk = file.slice(start, end); 173 const tsStart = Date.now(); 174 try { 175 const res = await retryFetch(urls[i], { method: 'PUT', body: chunk }, 3); 176 const tsEnd = Date.now(); 177 const speed = (end - start) / 1024 / 1024 / ((tsEnd - tsStart) / 1000); 178 speeds.push(speed); 179 parts.push({ PartNumber: i + 1, ETag: res.headers.get('ETag').replace(/"/g, '') }); 180 progress(parts.length / numChunks); 181 } catch (err) { 182 hasError = true; 183 console.error(`Chunk ${i + 1} failed after retries: ${err.message}`); 184 // Abort upload 185 try { 186 const abortResponse = await fetch(`${getWorkerUrl()}/abort`, { 187 method: 'POST', 188 headers: { 189 'Authorization': `Bearer ${jwt}`, 190 'Content-Type': 'application/json', 191 }, 192 body: JSON.stringify({ uploadId }), 193 }); 194 if (!abortResponse.ok) { 195 console.error('Abort failed:', await abortResponse.text()); 196 } 197 } catch (abortErr) { 198 console.error('Abort request failed:', abortErr.message); 199 } 200 error(err); 201 throw err; 202 } 203 }) 204 ); 205 } 206 207 try { 208 await Promise.all(uploadPromises); 209 if (hasError) { 210 throw new Error('Upload aborted due to chunk failure'); 211 } 212 } catch (e) { 213 error(e); 214 return; 215 } 216 217 // Update upload speed 218 if (speeds.length > 0) { 219 const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length; 220 localStorage.setItem('uploadSpeed', avgSpeed.toString()); 221 } 222 223 // Complete upload 224 try { 225 const completeResponse = await fetch(`${getWorkerUrl()}/complete`, { 226 method: 'POST', 227 headers: { 228 'Authorization': `Bearer ${jwt}`, 229 'Content-Type': 'application/json', 230 }, 231 body: JSON.stringify({ uploadId, parts }), 232 }); 233 if (completeResponse.status === 200) { 234 const responseData = await completeResponse.json(); 235 success({ ...responseData, status: completeResponse.status }); 236 } else { 237 error(new Error(await completeResponse.text() || `Complete failed: ${completeResponse.status}`)); 238 } 239 } catch (e) { 240 error(e); 241 } 242 } catch (e) { 243 error(e); 244 } 245 } 246 247 // SINGLE PUT UPLOAD 248 async function uploadFileSimple(file, params = {}, nodup = false) { 249 try { 250 params.Name = file.name.split('.')[0]; 251 params.Extension = (file.name.split('.').pop() || '').replace('jpeg', 'jpg'); 252 const jwt = await getJwtToken(params); 253 if (!jwt) return false; 254 255 const res = await fetch(`${getWorkerUrl()}/upload${nodup ? '?nodup=1' : ''}`, { 256 method: 'PUT', 257 headers: { 'Authorization': `Bearer ${jwt}` }, 258 body: file, 259 }); 260 return await res.json(); 261 } catch (e) { 262 console.error('Single PUT upload failed:', e.message); 263 return {}; 264 } 265 } 266 267 // REMOTE PULL UPLOAD 268 async function uploadFileRemote(url, params = {}, nodup = false) { 269 try { 270 params.remoteUrl = url; 271 const [name, ext] = url.split('?')[0].split('/').reverse()[0].split('.'); 272 if (name) params.Name = name; 273 if (ext) { 274 const extl = ext.toLowerCase().replace('jpeg', 'jpg'); 275 if (['bmp', 'gif', 'jpg', 'png'].includes(extl)) { 276 params.Extension = extl; 277 } 278 } 279 280 const jwt = await getJwtToken(params); 281 if (!jwt) return false;
282 283 const res = await fetch(`${getWorkerUrl()}/remote-upload${nodup ? '?nodup=1' : ''}`, { 284 method: 'POST', 285 headers: { 'Authorization': `Bearer ${jwt}` }, 286 }); 287 return res.ok ? await res.json() : {}; 288 } catch (e) { 289 console.error('Remote pull upload failed:', e.message); 290 return {}; 291 } 292 } 293 294 return { 295 uploadFile, 296 uploadFileSimple, 297 uploadFileRemote, 298 }; 299})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.