From 10937f1217d8a59b14790511601eb211f97137df Mon Sep 17 00:00:00 2001 From: flashwave Date: Fri, 2 Feb 2024 21:00:24 +0000 Subject: [PATCH] Added async version of EEPROM script. --- .gitignore | 2 + build.js | 102 +++++++++++++++ package-lock.json | 209 +++++++++++++++++++++++++++++++ package.json | 5 + public/js/eeprom-v1.0.js | 213 -------------------------------- scripts/assproc.js | 108 ++++++++++++++++ scripts/eepromv1.js/main.js | 195 +++++++++++++++++++++++++++++ scripts/eepromv1a.js/bytefmt.js | 45 +++++++ scripts/eepromv1a.js/main.js | 213 ++++++++++++++++++++++++++++++++ scripts/eepromv1a.js/xhr.js | 75 +++++++++++ scripts/utils.js | 35 ++++++ src/LandingRoutes.php | 2 +- 12 files changed, 990 insertions(+), 214 deletions(-) create mode 100644 build.js create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 public/js/eeprom-v1.0.js create mode 100644 scripts/assproc.js create mode 100644 scripts/eepromv1.js/main.js create mode 100644 scripts/eepromv1a.js/bytefmt.js create mode 100644 scripts/eepromv1a.js/main.js create mode 100644 scripts/eepromv1a.js/xhr.js create mode 100644 scripts/utils.js diff --git a/.gitignore b/.gitignore index 0901d3e..8787bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ .DS_Store /public/data/* /public/thumb/* +/public/scripts /public/robots.txt /config.cfg /config.ini /.debug /vendor /.migrating +/node_modules diff --git a/build.js b/build.js new file mode 100644 index 0000000..a2d0d35 --- /dev/null +++ b/build.js @@ -0,0 +1,102 @@ +// IMPORTS +const fs = require('fs'); +const swc = require('@swc/core'); +const path = require('path'); +const utils = require('./scripts/utils.js'); +const assproc = require('./scripts/assproc.js'); + + +// CONFIG +const rootDir = __dirname; +const srcDir = path.join(rootDir, 'scripts'); +const pubDir = path.join(rootDir, 'public'); +const pubAssetsDir = path.join(pubDir, 'scripts'); + +const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug')); + +const buildTasks = { + js: [ + { source: 'eepromv1.js', target: '/scripts', name: 'eepromv1.js', es: 'es5' }, + { source: 'eepromv1a.js', target: '/scripts', name: 'eepromv1a.js' }, + ], +}; + + +// PREP +const swcJscOptions = { + target: 'es2021', + loose: false, + externalHelpers: false, + keepClassNames: true, + preserveAllComments: false, + transform: {}, + parser: { + syntax: 'ecmascript', + jsx: false, + dynamicImport: false, + privateMethod: false, + functionBind: false, + exportDefaultFrom: false, + exportNamespaceFrom: false, + decorators: false, + decoratorsBeforeExport: false, + topLevelAwait: true, + importMeta: false, + }, +}; + + +// BUILD +(async () => { + const files = {}; + + console.log('Ensuring assets directory exists...'); + fs.mkdirSync(pubAssetsDir, { recursive: true }); + + + console.log(); + console.log('JS assets'); + for(const info of buildTasks.js) { + console.log(`=> Building ${info.source}...`); + + let origTarget = undefined; + if('es' in info) { + origTarget = swcJscOptions.target; + swcJscOptions.target = info.es; + } + + const assprocOpts = { + prefix: '#', + entry: info.entry || 'main.js', + }; + const swcOpts = { + filename: info.source, + sourceMaps: false, + isModule: false, + minify: !isDebugBuild, + jsc: swcJscOptions, + }; + + const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts) + .then(output => swc.transform(output, swcOpts)) + .then(output => { + const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) }); + const pubName = path.join(info.target || '', name); + + console.log(` Saving to ${pubName}...`); + fs.writeFileSync(path.join(pubDir, pubName), output.code); + + return pubName; + }); + + if(origTarget !== undefined) + swcJscOptions.target = origTarget; + + files[info.source] = pubName; + } + + + console.log(); + console.log('Cleaning up old builds...'); + assproc.housekeep(pubAssetsDir); +})(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4b37027 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,209 @@ +{ + "name": "eeprom.edgii.net", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@swc/core": "^1.3.107" + } + }, + "node_modules/@swc/core": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", + "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.107", + "@swc/core-darwin-x64": "1.3.107", + "@swc/core-linux-arm-gnueabihf": "1.3.107", + "@swc/core-linux-arm64-gnu": "1.3.107", + "@swc/core-linux-arm64-musl": "1.3.107", + "@swc/core-linux-x64-gnu": "1.3.107", + "@swc/core-linux-x64-musl": "1.3.107", + "@swc/core-win32-arm64-msvc": "1.3.107", + "@swc/core-win32-ia32-msvc": "1.3.107", + "@swc/core-win32-x64-msvc": "1.3.107" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz", + "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz", + "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz", + "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz", + "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz", + "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz", + "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz", + "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz", + "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz", + "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.107", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz", + "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==" + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ecb063 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@swc/core": "^1.3.107" + } +} diff --git a/public/js/eeprom-v1.0.js b/public/js/eeprom-v1.0.js deleted file mode 100644 index 6a62ba0..0000000 --- a/public/js/eeprom-v1.0.js +++ /dev/null @@ -1,213 +0,0 @@ -var EEPROM = function(srcId, endpoint, authorization) { - var obj = { - srcId: parseInt(srcId), - endpoint: endpoint, - authorization: authorization, - }; - - obj.setEndpoint = function(endpoint) { - obj.endpoint = (endpoint || '').toString(); - }; - - obj.setAuthorization = function(authorization) { - obj.authorization = (authorization || '').toString(); - }; - - obj.deleteUpload = function(fileInfo) { - return new EEPROM.EEPROMDeleteTask( - obj.authorization, - fileInfo - ); - }; - - obj.createUpload = function(file) { - return new EEPROM.EEPROMUploadTask( - obj.srcId, - obj.endpoint, - obj.authorization, - file - ); - }; - - return obj; -}; -EEPROM.ERR_GENERIC = 'generic'; -EEPROM.ERR_INVALID = 'invalid'; -EEPROM.ERR_AUTH = 'auth'; -EEPROM.ERR_ACCESS = 'access'; -EEPROM.ERR_DMCA = 'dmca'; -EEPROM.ERR_GONE = 'gone'; -EEPROM.ERR_SERVER = 'server'; -EEPROM.ERR_SIZE = 'size'; - -EEPROM.EEPROMFile = function(fileInfo) { - var obj = { - id: (fileInfo.id || '').toString(), - url: (fileInfo.url || '').toString(), - urlf: (fileInfo.urlf || '').toString(), - thumb: (fileInfo.thumb || '').toString(), - name: (fileInfo.name || '').toString(), - type: (fileInfo.type || '').toString(), - size: parseInt(fileInfo.size || 0), - user: parseInt(fileInfo.user || 0), - hash: (fileInfo.hash || '').toString(), - created: (fileInfo.created || null), - accessed: (fileInfo.accessed || null), - expires: (fileInfo.expires || null), - deleted: (fileInfo.deleted || null), - dmca: (fileInfo.dmca || null), - }; - - obj.isImage = function() { return obj.type.indexOf('image/') === 0; }; - obj.isAudio = function() { return obj.type === 'application/x-font-gdos' || obj.type.indexOf('audio/') === 0; }; - obj.isVideo = function() { return obj.type.indexOf('video/') === 0; }; - obj.isMedia = function() { return obj.isImage() || obj.isAudio() || obj.isVideo(); }; - - return obj; -}; - -EEPROM.EEPROMDeleteTask = function(authorization, fileInfo) { - var obj = { - authorization: authorization, - fileInfo: fileInfo, - onSuccess: undefined, - onFailure: undefined, - }; - - var xhr = obj.xhr = new XMLHttpRequest; - obj.xhr.addEventListener('readystatechange', function() { - if(xhr.readyState !== 4) - return; - - if(xhr.status !== 204) { - obj.errorCode = EEPROM.ERR_GENERIC; - - switch(xhr.status) { - case 401: - obj.errorCode = EEPROM.ERR_AUTH; - break; - case 403: - obj.errorCode = EEPROM.ERR_ACCESS; - break; - case 404: - case 410: - obj.errorCode = EEPROM.ERR_GONE; - break; - case 500: - case 503: - obj.errorCode = EEPROM.ERR_SERVER; - break; - } - - if(obj.onFailure) - obj.onFailure(obj.errorCode); - return; - } - - if(obj.onSuccess) - obj.onSuccess(); - }); - - obj.start = function() { - xhr.open('DELETE', obj.fileInfo.urlf); - if(obj.authorization) xhr.setRequestHeader('Authorization', obj.authorization); - else xhr.withCredentials = true; - xhr.send(); - }; - - return obj; -}; - -EEPROM.EEPROMUploadTask = function(srcId, endpoint, authorization, file) { - var obj = { - aborted: false, - endpoint: endpoint, - authorization: authorization, - onComplete: undefined, - onFailure: undefined, - onProgress: undefined, - failureResponse: undefined, - }; - - var xhr = obj.xhr = new XMLHttpRequest, - fd = obj.formData = new FormData; - - fd.append('src', srcId); - fd.append('file', file); - - var reportUploadProgress = function(ev) { - if(obj.onProgress) - obj.onProgress({ - loaded: ev.loaded, - total: ev.total, - progress: Math.ceil((ev.loaded / ev.total) * 100), - }); - }; - - xhr.upload.addEventListener('loadstart', reportUploadProgress); - xhr.upload.addEventListener('progress', reportUploadProgress); - xhr.upload.addEventListener('load', reportUploadProgress); - - xhr.addEventListener('readystatechange', function() { - if(this.readyState !== 4) - return; - - if(this.status !== 201) { - obj.failureResponse = { - userAborted: obj.aborted, - error: EEPROM.ERR_GENERIC, - }; - - switch(this.status) { - case 400: - case 405: - obj.failureResponse.error = EEPROM.ERR_INVALID; - break; - case 401: - obj.failureResponse.error = EEPROM.ERR_AUTH; - break; - case 403: - obj.failureResponse.error = EEPROM.ERR_ACCESS; - break; - case 404: - case 410: - obj.failureResponse.error = EEPROM.ERR_GONE; - break; - case 451: - obj.failureResponse.error = EEPROM.ERR_DMCA; - break; - case 413: - obj.failureResponse.error = EEPROM.ERR_SIZE; - obj.failureResponse.maxSize = parseInt(this.getResponseHeader('X-EEPROM-Max-Size')); - break; - case 500: - case 503: - obj.failureResponse.error = EEPROM.ERR_SERVER; - break; - } - - if(obj.onFailure) - obj.onFailure(obj.failureResponse); - return; - } - - obj.fileInfo = new EEPROM.EEPROMFile(JSON.parse(this.responseText)); - - if(obj.onComplete) - obj.onComplete(obj.fileInfo); - }); - - obj.abort = function() { - obj.aborted = true; - xhr.abort(); - }; - - obj.start = function() { - xhr.open('POST', obj.endpoint); - if(obj.authorization) xhr.setRequestHeader('Authorization', obj.authorization); - else xhr.withCredentials = true; - xhr.send(fd); - }; - - return obj; -}; diff --git a/scripts/assproc.js b/scripts/assproc.js new file mode 100644 index 0000000..1150b94 --- /dev/null +++ b/scripts/assproc.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const utils = require('./utils.js'); + +exports.process = async function(root, options) { + const macroPrefix = options.prefix || '#'; + const entryPoint = options.entry || ''; + + root = fs.realpathSync(root); + + const included = []; + + const processFile = async function(fileName) { + const fullPath = path.join(root, fileName); + if(included.includes(fullPath)) + return ''; + included.push(fullPath); + + if(!fullPath.startsWith(root)) + return '/* *** INVALID PATH: ' + fullPath + ' */'; + if(!fs.existsSync(fullPath)) + return '/* *** FILE NOT FOUND: ' + fullPath + ' */'; + + const lines = readline.createInterface({ + input: fs.createReadStream(fullPath), + crlfDelay: Infinity, + }); + + let output = ''; + let lastWasEmpty = false; + + if(options.showPath) + output += "/* *** PATH: " + fullPath + " */\n"; + + for await(const line of lines) { + const lineTrimmed = utils.trim(line); + if(lineTrimmed === '') + continue; + + if(line.startsWith(macroPrefix)) { + const args = lineTrimmed.split(' '); + const macro = utils.trim(utils.trimStart(args.shift(), macroPrefix)); + + switch(macro) { + case 'comment': + break; + + case 'include': { + const includePath = utils.trimEnd(args.join(' '), ';'); + output += utils.trim(await processFile(includePath)); + output += "\n"; + break; + } + + case 'buildvars': + if(typeof options.buildVars === 'object') { + const bvTarget = options.buildVarsTarget || 'window'; + const bvProps = []; + + for(const bvName in options.buildVars) + bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])}, writable: false }`); + + if(Object.keys(bvProps).length > 0) + output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`; + } + break; + + default: + output += line; + output += "\n"; + break; + } + } else { + output += line; + output += "\n"; + } + } + + return output; + }; + + return await processFile(entryPoint); +}; + +exports.housekeep = function(assetsPath) { + const files = fs.readdirSync(assetsPath).map(fileName => { + const stats = fs.statSync(path.join(assetsPath, fileName)); + return { + name: fileName, + lastMod: stats.mtimeMs, + }; + }).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name); + + const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i; + const counts = {}; + + for(const fileName of files) { + const match = fileName.match(regex); + if(match) { + const name = match[1] + '-' + match[3]; + counts[name] = (counts[name] || 0) + 1; + + if(counts[name] > 5) + fs.unlinkSync(path.join(assetsPath, fileName)); + } + } +}; diff --git a/scripts/eepromv1.js/main.js b/scripts/eepromv1.js/main.js new file mode 100644 index 0000000..f72f110 --- /dev/null +++ b/scripts/eepromv1.js/main.js @@ -0,0 +1,195 @@ +window.EEPROM = (() => { + const errGeneric = 'generic'; + const errInvalid = 'invalid'; + const errAuth = 'auth'; + const errAccess = 'access'; + const errDMCA = 'dmca'; + const errGone = 'gone'; + const errSize = 'size'; + const errServer = 'server'; + + const createClient = function(srcId, endPoint, authorization) { + if(typeof srcId !== 'number') + srcId = parseInt(srcId); + if(typeof endPoint !== 'string') + throw 'endPoint must be a string'; + if(typeof authorization !== 'string' && typeof authorization !== 'function') + throw 'authorization must be a string or a function returning a string'; + + const createUpload = file => { + const uploadTask = { onComplete: undefined, onFailure: undefined, onProgress: undefined }; + let userAborted = false; + + const xhr = new XMLHttpRequest; + const fd = new FormData; + + fd.append('src', srcId); + fd.append('file', file); + + const reportUploadProgress = ev => { + if(typeof uploadTask.onProgress === 'function') + uploadTask.onProgress({ + loaded: ev.loaded, + total: ev.total, + progress: Math.ceil((ev.loaded / ev.total) * 100), + }); + }; + + xhr.upload.addEventListener('loadstart', reportUploadProgress); + xhr.upload.addEventListener('progress', reportUploadProgress); + xhr.upload.addEventListener('load', reportUploadProgress); + + xhr.addEventListener('readystatechange', () => { + if(xhr.readyState !== 4) + return; + + if(xhr.status !== 201) { + const failureResponse = { + userAborted: userAborted, + error: errGeneric, + }; + + switch(xhr.status) { + case 400: + case 405: + failureResponse.error = errInvalid; + break; + case 401: + failureResponse.error = errAuth; + break; + case 403: + failureResponse.error = errAccess; + break; + case 404: + case 410: + failureResponse.error = errGone; + break; + case 451: + failureResponse.error = errDMCA; + break; + case 413: + failureResponse.error = errSize; + failureResponse.maxSize = parseInt(xhr.getResponseHeader('X-EEPROM-Max-Size')); + break; + case 500: + case 503: + failureResponse.error = errServer; + break; + } + + if(typeof uploadTask.onFailure === 'function') + uploadTask.onFailure(failureResponse); + return; + } + + if(typeof uploadTask.onComplete === 'function') { + const fileInfo = JSON.parse(xhr.responseText); + if(typeof fileInfo !== 'object') { + if(typeof uploadTask.onFailure === 'function') + uploadTask.onFailure({ + userAborted: userAborted, + error: errServer, + }); + return; + } + + fileInfo.isImage = () => typeof fileInfo.type === 'string' && fileInfo.type.indexOf('image/') === 0; + fileInfo.isVideo = () => typeof fileInfo.type === 'string' && fileInfo.type.indexOf('video/') === 0; + fileInfo.isAudio = () => typeof fileInfo.type === 'string' && (fileInfo.type === 'application/x-font-gdos' || fileInfo.type.indexOf('audio/') === 0); + fileInfo.isMedia = () => typeof fileInfo.type === 'string' && (fileInfo.isImage() || fileInfo.isAudio() || fileInfo.isVideo()); + + uploadTask.onComplete(fileInfo); + } + }); + + uploadTask.abort = () => { + userAborted = true; + xhr.abort(); + }; + + uploadTask.start = () => { + xhr.open('POST', endPoint); + + const authIsFunc = typeof authorization === 'function'; + if(authIsFunc || typeof authorization === 'string') + xhr.setRequestHeader('Authorization', authIsFunc ? authorization() : authorization); + else + xhr.withCredentials = true; + + xhr.send(fd); + }; + + return uploadTask; + }; + + const deleteUpload = fileInfo => { + const deleteTask = { onSuccess: undefined, onFailure: undefined }; + + const xhr = new XMLHttpRequest; + xhr.addEventListener('readystatechange', () => { + if(xhr.readyState !== 4) + return; + + if(xhr.status !== 204) { + let errorCode = errGeneric; + + switch(xhr.status) { + case 401: + errorCode = errAuth; + break; + case 403: + errorCode = errAccess; + break; + case 404: + case 410: + errorCode = errGone; + break; + case 500: + case 503: + errorCode = errServer; + break; + } + + if(typeof deleteTask.onFailure === 'function') + deleteTask.onFailure(errorCode); + return; + } + + if(typeof deleteTask.onSuccess === 'function') + deleteTask.onSuccess(); + }); + + deleteTask.start = () => { + xhr.open('DELETE', fileInfo.urlf); + + const authIsFunc = typeof authorization === 'function'; + if(authIsFunc || typeof authorization === 'string') + xhr.setRequestHeader('Authorization', authIsFunc ? authorization() : authorization); + else + xhr.withCredentials = true; + + xhr.send(); + }; + + return deleteTask; + }; + + return { + createUpload: createUpload, + deleteUpload: deleteUpload, + }; + }; + + Object.defineProperties(createClient, { + ERR_GENERIC: { value: errGeneric }, + ERR_INVALID: { value: errInvalid }, + ERR_AUTH: { value: errAuth }, + ERR_ACCESS: { value: errAccess }, + ERR_DMCA: { value: errDMCA }, + ERR_GONE: { value: errGone }, + ERR_SERVER: { value: errServer }, + ERR_SIZE: { value: errSize }, + }); + + return createClient; +})(); diff --git a/scripts/eepromv1a.js/bytefmt.js b/scripts/eepromv1a.js/bytefmt.js new file mode 100644 index 0000000..a051293 --- /dev/null +++ b/scripts/eepromv1a.js/bytefmt.js @@ -0,0 +1,45 @@ +const EEPFMT = (() => { + const symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; + + const format = (bytes, binary) => { + if(bytes === 0) + return 'Zero Bytes'; + + const negative = bytes < 0; + const power = binary ? 1024 : 1000; + const exp = Math.floor(Math.log(bytes) / Math.log(power)); + + bytes = Math.abs(bytes); + + const number = bytes / Math.pow(power, exp); + const symbol = symbols[exp]; + + let string = ''; + if(negative) + string += '-'; + + const fractionDigits = bytes < power ? 0 : (number < 10 ? 2 : 1); + string += number.toLocaleString(undefined, { + maximumFractionDigits: fractionDigits, + minimumFractionDigits: fractionDigits, + }); + + string += ` ${symbol}`; + + if(symbol === '') { + string += 'Byte'; + if(number > 1) + string += 's'; + } else { + if(binary) + string += 'i'; + string += 'B'; + } + + return string; + }; + + return { + format: format, + }; +})(); diff --git a/scripts/eepromv1a.js/main.js b/scripts/eepromv1a.js/main.js new file mode 100644 index 0000000..8bba72d --- /dev/null +++ b/scripts/eepromv1a.js/main.js @@ -0,0 +1,213 @@ +#include bytefmt.js +#include xhr.js + +const EEPROM = function(appId, endPoint, auth) { + if(typeof appId !== 'string') + appId = (appId || '').toString(); + if(typeof endPoint !== 'string') + throw 'endPoint must be a string'; + + const applyAuth = options => { + if(typeof auth === 'function') + options.headers.Authorization = auth(); + else if(typeof auth === 'string') + options.headers.Authorization = auth; + else + options.authed = true; + }; + + const createUpload = fileInput => { + if(!(fileInput instanceof File)) + throw 'fileInput must be an instance of window.File'; + + let userAborted = false; + let abortHandler, progressHandler; + + const reportProgress = ev => { + if(progressHandler !== undefined) + progressHandler({ + loaded: ev.loaded, + total: ev.total, + progress: ev.total <= 0 ? 0 : ev.loaded / ev.total, + }); + }; + + const uploadAbortedError = () => { + return { + error: 'eeprom:aborted_error', + aborted: userAborted, + toString: () => 'File upload was aborted manually.', + }; + }; + + const uploadGenericError = () => { + return { + error: 'eeprom:generic_error', + aborted: userAborted, + toString: () => 'File upload failed for unknown reasons.', + }; + }; + + return { + abort: () => { + userAborted = true; + if(abortHandler !== undefined) + abortHandler(); + }, + onProgress: handler => { + if(typeof handler !== 'function') + throw 'handler must be a function'; + progressHandler = handler; + }, + start: async () => { + if(userAborted) + throw uploadAbortedError(); + + const options = { + type: 'json', + headers: {}, + upload: reportProgress, + abort: handler => abortHandler = handler, + }; + + applyAuth(options); + + const formData = new FormData; + formData.append('src', appId); + formData.append('file', fileInput); + + try { + const result = await EEPXHR.post(`${endPoint}/uploads`, options, formData); + + if(result.status !== 201) { + if(result.status === 400) + throw { + error: 'eeprom:request_error', + aborted: userAborted, + toString: () => 'There was an error with the upload request.', + }; + if(result.status === 401) + throw { + error: 'eeprom:auth_error', + aborted: userAborted, + toString: () => 'Could not authenticate upload request. If this persists, try refreshing the page.', + }; + if(result.status === 403) + throw { + error: 'eeprom:access_error', + aborted: userAborted, + toString: () => 'You are not allowed to upload files.', + }; + if(result.status === 404) + throw { + error: 'eeprom:app_error', + aborted: userAborted, + toString: () => 'EEPROM app is not configured properly.', + }; + if(result.status === 413) { + const maxSize = parseInt(xhr.headers().get('x-eeprom-max-size')); + const maxSizeFormatted = EEPFMT.format(maxSize); + throw { + error: 'eeprom:size_error', + maxSize: maxSize, + maxSizeFormatted: maxSizeFormatted, + aborted: userAborted, + toString: () => maxSize < 1 ? 'Uploaded file was too large.' : `Uploads may not be larger than ${maxSizeFormatted}.`, + }; + } + if(result.status === 451) + throw { + error: 'eeprom:dmca_error', + aborted: userAborted, + toString: () => 'This file is blocked from being uploaded, possibly for copyright reasons.', + }; + if(result.status === 500) + throw { + error: 'eeprom:server_error', + aborted: userAborted, + toString: () => 'An error occurred within the EEPROM server. If this persists, report it to a developer.', + }; + if(result.status === 503) + throw { + error: 'eeprom:maintenance_error', + aborted: userAborted, + toString: () => 'EEPROM server is temporarily unavailable for maintenance.', + }; + + throw uploadGenericError(); + } + + const fileInfo = result.body(); + + if(typeof fileInfo.type === 'string') { + fileInfo.isImage = () => fileInfo.type.indexOf('image/') === 0; + fileInfo.isVideo = () => fileInfo.type.indexOf('video/') === 0; + fileInfo.isAudio = () => fileInfo.type === 'application/x-font-gdos' || fileInfo.type.indexOf('audio/') === 0; + } else + fileInfo.isImage = fileInfo.isVideo = fileInfo.isAudio = () => false; + + fileInfo.isMedia = () => fileInfo.isImage() || fileInfo.isAudio() || fileInfo.isVideo(); + + return Object.freeze(fileInfo); + } catch(ex) { + if(!ex.abort) { + console.error(ex); + throw uploadGenericError(); + } + + throw uploadAbortedError(); + } + }, + }; + }; + + const deleteUpload = async fileInfo => { + if(typeof fileInfo !== 'object') + throw 'fileInfo must be an object'; + if(typeof fileInfo.urlf !== 'string') + throw 'fileInfo.urlf must be a string'; + + const options = { headers: {} }; + applyAuth(options); + + const result = await EEPXHR.delete(fileInfo.urlf, options); + + if(result.status !== 204) { + if(result.status === 401) + throw { + error: 'eeprom:auth_error', + toString: () => 'Could not authenticate delete request. If this persists, try refreshing the page.', + }; + if(result.status === 403) + throw { + error: 'eeprom:access_error', + toString: () => 'You are not allowed to delete this file.', + }; + if(result.status === 404) + throw { + error: 'eeprom:file_error', + toString: () => 'File not found.', + }; + if(result.status === 500) + throw { + error: 'eeprom:server_error', + toString: () => 'An error occurred within the EEPROM server. If this persists, report it to a developer.', + }; + if(result.status === 503) + throw { + error: 'eeprom:maintenance_error', + toString: () => 'EEPROM server is temporarily unavailable for maintenance.', + }; + + throw { + error: 'eeprom:generic_error', + toString: () => 'File delete failed for unknown reasons.', + }; + } + }; + + return { + create: createUpload, + delete: deleteUpload, + }; +}; diff --git a/scripts/eepromv1a.js/xhr.js b/scripts/eepromv1a.js/xhr.js new file mode 100644 index 0000000..036724f --- /dev/null +++ b/scripts/eepromv1a.js/xhr.js @@ -0,0 +1,75 @@ +const EEPXHR = (() => { + const send = function(method, url, options, body) { + const xhr = new XMLHttpRequest; + const requestHeaders = new Map; + + if('headers' in options && typeof options.headers === 'object') + for(const name in options.headers) + if(options.headers.hasOwnProperty(name)) + requestHeaders.set(name.toLowerCase(), options.headers[name]); + + if(typeof options.upload === 'function') { + xhr.upload.onloadstart = ev => options.upload(ev); + xhr.upload.onprogress = ev => options.upload(ev); + xhr.upload.onloadend = ev => options.upload(ev); + } + + if(options.authed) + xhr.withCredentials = true; + + if(typeof options.timeout === 'number') + xhr.timeout = options.timeout; + + if(typeof options.type === 'string') + xhr.responseType = options.type; + + if(typeof options.abort === 'function') + options.abort(() => xhr.abort()); + + return new Promise((resolve, reject) => { + let responseHeaders = undefined; + + xhr.onload = ev => resolve({ + status: xhr.status, + body: () => xhr.response, + headers: () => { + if(responseHeaders !== undefined) + return responseHeaders; + + responseHeaders = new Map; + + const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); + for(const name in raw) + if(raw.hasOwnProperty(name)) { + const parts = raw[name].split(': '); + responseHeaders.set(parts.shift(), parts.join(': ')); + } + + return responseHeaders; + }, + }); + + xhr.onabort = ev => reject({ + abort: true, + xhr: xhr, + ev: ev, + }); + + xhr.onerror = ev => reject({ + abort: false, + xhr: xhr, + ev: ev, + }); + + xhr.open(method, url); + for(const [name, value] of requestHeaders) + xhr.setRequestHeader(name, value); + xhr.send(body); + }); + }; + + return { + post: (url, options, body) => send('POST', url, options, body), + delete: (url, options, body) => send('DELETE', url, options, body), + }; +})(); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..d825d83 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); + +exports.strtr = (str, replacements) => str.toString().replace( + /{([^}]+)}/g, (match, key) => replacements[key] || match +); + +const trim = function(str, chars, flags) { + if(chars === undefined) + chars = " \n\r\t\v\0"; + + let start = 0, + end = str.length; + + if(flags & 0x01) + while(start < end && chars.indexOf(str[start]) >= 0) + ++start; + + if(flags & 0x02) + while(end > start && chars.indexOf(str[end - 1]) >= 0) + --end; + + return (start > 0 || end < str.length) + ? str.substring(start, end) + : str; +}; + +exports.trimStart = (str, chars) => trim(str, chars, 0x01); +exports.trimEnd = (str, chars) => trim(str, chars, 0x02); +exports.trim = (str, chars) => trim(str, chars, 0x03); + +exports.shortHash = function(text) { + const hash = crypto.createHash('sha256'); + hash.update(text); + return hash.digest('hex').substring(0, 8); +}; diff --git a/src/LandingRoutes.php b/src/LandingRoutes.php index e528fb6..08cfcfc 100644 --- a/src/LandingRoutes.php +++ b/src/LandingRoutes.php @@ -42,7 +42,7 @@ class LandingRoutes extends RouteHandler { #[Route('GET', '/eeprom.js')] public function getEepromJs($response) { - $response->accelRedirect('/js/eeprom-v1.0.js'); + $response->accelRedirect('/scripts/eepromv1.js'); $response->setContentType('application/javascript; charset=utf-8'); } }