UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

229 lines • 9.53 kB
// workaround for this is not a function when deployed // see https://github.com/pvorb/node-md5/issues/52 import md5 from "md5"; import { FileLoader } from "three"; import { showBalloonWarning } from "./debug/index.js"; import { hasCommercialLicense } from "./engine_license.js"; import { delay } from "./engine_utils.js"; export var BlobStorage; (function (BlobStorage) { const maxSizeInMB = 50; const maxFreeSizeInMB = 5; /** The base url for the blob storage. * The expected endpoints are: * - POST `/api/needle/blob` - to request a new upload url */ BlobStorage.baseUrl = "https://networking.needle.tools"; /** * Generates an md5 hash from a given buffer * @param buffer The buffer to hash * @returns The md5 hash */ function hashMD5(buffer) { return md5(new Uint8Array(buffer)); } BlobStorage.hashMD5 = hashMD5; function hashMD5_Base64(buffer) { const bytes = md5(new Uint8Array(buffer), { encoding: "binary", asBytes: true }); return btoa(String.fromCharCode(...bytes)); } BlobStorage.hashMD5_Base64 = hashMD5_Base64; function hashSha256(buffer) { const bytes = new Uint8Array(buffer); const hash = crypto.subtle.digest('SHA-256', bytes).then(res => { return btoa(String.fromCharCode(...new Uint8Array(res))); }); return hash; } BlobStorage.hashSha256 = hashSha256; /** * Checks if the current user can upload a file of the given size * @param info The file info */ function canUpload(info) { const sizeInMB = info.filesize / 1024 / 1024; if (hasCommercialLicense()) { return sizeInMB < maxSizeInMB; } return sizeInMB < maxFreeSizeInMB; } BlobStorage.canUpload = canUpload; async function upload(file, opts) { const _baseUrl = BlobStorage.baseUrl; if (!_baseUrl) { console.error("Blob storage base url is not set"); return null; } else if (!file.name) { console.error("Upload: file name is missing"); return null; } let arrayBuffer = null; if (file instanceof File) { arrayBuffer = await file.arrayBuffer(); } else { arrayBuffer = file.data; } const filesize = arrayBuffer.byteLength; const filesizeInMB = filesize / 1024 / 1024; if (filesizeInMB > maxSizeInMB) { if (opts?.silent !== true) showBalloonWarning(`File (${filesizeInMB.toFixed(1)}MB) is too large for uploading (see console for details)`); console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max allowed size is ${maxSizeInMB}MB`); return null; } else if (!hasCommercialLicense() && filesizeInMB > maxFreeSizeInMB) { if (opts?.silent !== true) showBalloonWarning(`File is too large for uploading. Please get a <a href=\"https://needle.tools/pricing\" target=\"_blank\">commercial license</a> to upload files larger than 5MB`); console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max size is 5MB for non-commercial users. Please get a commercial license at https://needle.tools/pricing for larger files (up to 50MB)`); return null; } else if (filesize < 1) { console.warn(`Your file is too small for uploading (${filesizeInMB.toFixed(1)}MB). Min size is 1 byte`); return null; } const hash = hashMD5_Base64(arrayBuffer); const headers = { filename: file.name, "Content-Md5": hash, // "x-amz-checksum-sha256": checksum, // "X-Amz-Content-Sha256": checksum, "Content-Type": file.type || "application/octet-stream", "FileSize": filesize.toString(), // enforced by the server "Content-Disposition": `attachment; filename=\"${file.name}\"`, // enforced by the server "x-amz-server-side-encryption": "AES256", }; const uploadResult = await fetch(_baseUrl + "/api/needle/blob", { method: "POST", headers, signal: opts?.abort, }) .then(res => res.json()) .catch(err => { console.error(err); return null; }); if (uploadResult == null) { console.warn("Upload failed..."); return null; } else if ("error" in uploadResult) { console.error(uploadResult.error); return null; } // If the server responded with a upload url, we can now upload the file else if ("upload" in uploadResult && uploadResult.upload) { console.debug("Uploading file", uploadResult.upload); let didUpload = false; let error = null; // try uploading the file 5 times for (let i = 0; i < 3; i++) { try { if (didUpload) break; if (opts?.abort?.aborted) { console.debug("Aborted upload"); return null; } const res = await tryUpload(uploadResult.upload); if (res instanceof Error) { error = res; await delay(1000 * i); } else if (res.ok) { console.debug("File uploaded successfully"); didUpload = true; } } catch (err) { console.error(err); } } if (!didUpload) { console.error(error?.message || "Failed to upload file"); return null; } function tryUpload(url) { opts?.onProgress?.call(null, { progress01: 0, state: "inprogress" }); const uploadRes = fetch(url, { method: "PUT", headers, body: arrayBuffer, signal: opts?.abort, }) .then(res => { opts?.onProgress?.call(null, { progress01: 1, state: "finished" }); return res; }) .catch(err => { return err; }); return uploadRes; } } // Provide the download url to the caller if ("download" in uploadResult) { const downloadUrl = _baseUrl + uploadResult.download; console.debug("File found in blob storage", downloadUrl); return { key: uploadResult.key, success: true, download_url: downloadUrl, }; } return null; } BlobStorage.upload = upload; function getBlobUrlForKey(key) { return `${BlobStorage.baseUrl}/api/needle/blob/${key}`; } BlobStorage.getBlobUrlForKey = getBlobUrlForKey; async function download(url, progressCallback) { // Using a FileLoader here instead of manually fetching so we're able to use the three.js cache system const loader = new FileLoader(); loader.setResponseType('arraybuffer'); // loader.setRequestHeader( this.requestHeader ); // loader.setWithCredentials( this.withCredentials ); const res = await loader.loadAsync(url, prog => { if (progressCallback) { progressCallback.call(null, prog); } }); if (!(res instanceof ArrayBuffer)) { console.error("Download failed, no arraybuffer returned"); return null; } return new Uint8Array(res); // Old solution: this didn't re-use the three.js loading cache. Using a FileLoader the GLTFLoader under the hood is smart enough to NOT start a new download request // const response = await fetch(url); // const reader = response.body?.getReader(); // const contentLength = response.headers.get('Content-Length'); // const total = contentLength ? parseInt(contentLength) : 0; // if (!reader) return null; // let received: number = 0; // const chunks: Uint8Array[] = []; // while (true) { // const { done, value } = await reader.read(); // if (value) { // chunks.push(value); // received += value.length; // progressCallback?.call(null, new ProgressEvent('progress', { loaded: received, total: total })); // } // if (done) { // break; // } // } // const final = new Uint8Array(received); // let position = 0; // for (const chunk of chunks) { // final.set(chunk, position); // position += chunk.length; // } // return final; } BlobStorage.download = download; })(BlobStorage || (BlobStorage = {})); //# sourceMappingURL=engine_networking_blob.js.map