Inital build
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s

This commit is contained in:
Elijah 2026-05-22 12:29:43 -07:00
parent fa2be029a2
commit 724d70e58b
3339 changed files with 1075535 additions and 0 deletions

View file

@ -0,0 +1,51 @@
import isReactNative from './isReactNative.js'
import uriToBlob from './uriToBlob.js'
import FileSource from './sources/FileSource.js'
import StreamSource from './sources/StreamSource.js'
export default class FileReader {
async openFile(input, chunkSize) {
// In React Native, when user selects a file, instead of a File or Blob,
// you usually get a file object {} with a uri property that contains
// a local path to the file. We use XMLHttpRequest to fetch
// the file blob, before uploading with tus.
if (isReactNative() && input && typeof input.uri !== 'undefined') {
try {
const blob = await uriToBlob(input.uri)
return new FileSource(blob)
} catch (err) {
throw new Error(
`tus: cannot fetch \`file.uri\` as Blob, make sure the uri is correct and accessible. ${err}`,
)
}
}
// Since we emulate the Blob type in our tests (not all target browsers
// support it), we cannot use `instanceof` for testing whether the input value
// can be handled. Instead, we simply check is the slice() function and the
// size property are available.
if (typeof input.slice === 'function' && typeof input.size !== 'undefined') {
return Promise.resolve(new FileSource(input))
}
if (typeof input.read === 'function') {
chunkSize = Number(chunkSize)
if (!Number.isFinite(chunkSize)) {
return Promise.reject(
new Error(
'cannot create source for stream without a finite value for the `chunkSize` option',
),
)
}
return Promise.resolve(new StreamSource(input, chunkSize))
}
return Promise.reject(
new Error(
'source object may only be an instance of File, Blob, or Reader in this environment',
),
)
}
}

View file

@ -0,0 +1,41 @@
import isReactNative from './isReactNative.js'
// TODO: Differenciate between input types
/**
* Generate a fingerprint for a file which will be used the store the endpoint
*
* @param {File} file
* @param {Object} options
* @param {Function} callback
*/
export default function fingerprint(file, options) {
if (isReactNative()) {
return Promise.resolve(reactNativeFingerprint(file, options))
}
return Promise.resolve(
['tus-br', file.name, file.type, file.size, file.lastModified, options.endpoint].join('-'),
)
}
function reactNativeFingerprint(file, options) {
const exifHash = file.exif ? hashCode(JSON.stringify(file.exif)) : 'noexif'
return ['tus-rn', file.name || 'noname', file.size || 'nosize', exifHash, options.endpoint].join(
'/',
)
}
function hashCode(str) {
// from https://stackoverflow.com/a/8831937/151666
let hash = 0
if (str.length === 0) {
return hash
}
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash &= hash // Convert to 32bit integer
}
return hash
}

View file

@ -0,0 +1,97 @@
export default class XHRHttpStack {
createRequest(method, url) {
return new Request(method, url)
}
getName() {
return 'XHRHttpStack'
}
}
class Request {
constructor(method, url) {
this._xhr = new XMLHttpRequest()
this._xhr.open(method, url, true)
this._method = method
this._url = url
this._headers = {}
}
getMethod() {
return this._method
}
getURL() {
return this._url
}
setHeader(header, value) {
this._xhr.setRequestHeader(header, value)
this._headers[header] = value
}
getHeader(header) {
return this._headers[header]
}
setProgressHandler(progressHandler) {
// Test support for progress events before attaching an event listener
if (!('upload' in this._xhr)) {
return
}
this._xhr.upload.onprogress = (e) => {
if (!e.lengthComputable) {
return
}
progressHandler(e.loaded)
}
}
send(body = null) {
return new Promise((resolve, reject) => {
this._xhr.onload = () => {
resolve(new Response(this._xhr))
}
this._xhr.onerror = (err) => {
reject(err)
}
this._xhr.send(body)
})
}
abort() {
this._xhr.abort()
return Promise.resolve()
}
getUnderlyingObject() {
return this._xhr
}
}
class Response {
constructor(xhr) {
this._xhr = xhr
}
getStatus() {
return this._xhr.status
}
getHeader(header) {
return this._xhr.getResponseHeader(header)
}
getBody() {
return this._xhr.responseText
}
getUnderlyingObject() {
return this._xhr
}
}

View file

@ -0,0 +1,45 @@
import DetailedError from '../error.js'
import { enableDebugLog } from '../logger.js'
import NoopUrlStorage from '../noopUrlStorage.js'
import BaseUpload from '../upload.js'
import FileReader from './fileReader.js'
import fingerprint from './fileSignature.js'
import DefaultHttpStack from './httpStack.js'
import { WebStorageUrlStorage, canStoreURLs } from './urlStorage.js'
const defaultOptions = {
...BaseUpload.defaultOptions,
httpStack: new DefaultHttpStack(),
fileReader: new FileReader(),
urlStorage: canStoreURLs ? new WebStorageUrlStorage() : new NoopUrlStorage(),
fingerprint,
}
class Upload extends BaseUpload {
constructor(file = null, options = {}) {
options = { ...defaultOptions, ...options }
super(file, options)
}
static terminate(url, options = {}) {
options = { ...defaultOptions, ...options }
return BaseUpload.terminate(url, options)
}
}
// Note: We don't reference `window` here because these classes also exist in a Web Worker's context.
const isSupported =
typeof XMLHttpRequest === 'function' &&
typeof Blob === 'function' &&
typeof Blob.prototype.slice === 'function'
export {
Upload,
canStoreURLs,
defaultOptions,
isSupported,
enableDebugLog,
DefaultHttpStack,
DetailedError,
}

View file

@ -0,0 +1,6 @@
const isReactNative = () =>
typeof navigator !== 'undefined' &&
typeof navigator.product === 'string' &&
navigator.product.toLowerCase() === 'reactnative'
export default isReactNative

View file

@ -0,0 +1,27 @@
import isCordova from './isCordova.js'
import readAsByteArray from './readAsByteArray.js'
export default class FileSource {
// Make this.size a method
constructor(file) {
this._file = file
this.size = file.size
}
slice(start, end) {
// In Apache Cordova applications, a File must be resolved using
// FileReader instances, see
// https://cordova.apache.org/docs/en/8.x/reference/cordova-plugin-file/index.html#read-a-file
if (isCordova()) {
return readAsByteArray(this._file.slice(start, end))
}
const value = this._file.slice(start, end)
const done = end >= this.size
return Promise.resolve({ value, done })
}
close() {
// Nothing to do here since we don't need to release any resources.
}
}

View file

@ -0,0 +1,89 @@
function len(blobOrArray) {
if (blobOrArray === undefined) return 0
if (blobOrArray.size !== undefined) return blobOrArray.size
return blobOrArray.length
}
/*
Typed arrays and blobs don't have a concat method.
This function helps StreamSource accumulate data to reach chunkSize.
*/
function concat(a, b) {
if (a.concat) {
// Is `a` an Array?
return a.concat(b)
}
if (a instanceof Blob) {
return new Blob([a, b], { type: a.type })
}
if (a.set) {
// Is `a` a typed array?
const c = new a.constructor(a.length + b.length)
c.set(a)
c.set(b, a.length)
return c
}
throw new Error('Unknown data type')
}
export default class StreamSource {
constructor(reader) {
this._buffer = undefined
this._bufferOffset = 0
this._reader = reader
this._done = false
}
slice(start, end) {
if (start < this._bufferOffset) {
return Promise.reject(new Error("Requested data is before the reader's current offset"))
}
return this._readUntilEnoughDataOrDone(start, end)
}
_readUntilEnoughDataOrDone(start, end) {
const hasEnoughData = end <= this._bufferOffset + len(this._buffer)
if (this._done || hasEnoughData) {
const value = this._getDataFromBuffer(start, end)
const done = value == null ? this._done : false
return Promise.resolve({ value, done })
}
return this._reader.read().then(({ value, done }) => {
if (done) {
this._done = true
} else if (this._buffer === undefined) {
this._buffer = value
} else {
this._buffer = concat(this._buffer, value)
}
return this._readUntilEnoughDataOrDone(start, end)
})
}
_getDataFromBuffer(start, end) {
// Remove data from buffer before `start`.
// Data might be reread from the buffer if an upload fails, so we can only
// safely delete data when it comes *before* what is currently being read.
if (start > this._bufferOffset) {
this._buffer = this._buffer.slice(start - this._bufferOffset)
this._bufferOffset = start
}
// If the buffer is empty after removing old data, all data has been read.
const hasAllDataBeenRead = len(this._buffer) === 0
if (this._done && hasAllDataBeenRead) {
return null
}
// We already removed data before `start`, so we just return the first
// chunk from the buffer.
return this._buffer.slice(0, end - start)
}
close() {
if (this._reader.cancel) {
this._reader.cancel()
}
}
}

View file

@ -0,0 +1,7 @@
const isCordova = () =>
typeof window !== 'undefined' &&
(typeof window.PhoneGap !== 'undefined' ||
typeof window.Cordova !== 'undefined' ||
typeof window.cordova !== 'undefined')
export default isCordova

View file

@ -0,0 +1,18 @@
/**
* readAsByteArray converts a File object to a Uint8Array.
* This function is only used on the Apache Cordova platform.
* See https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/index.html#read-a-file
*/
export default function readAsByteArray(chunk) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const value = new Uint8Array(reader.result)
resolve({ value })
}
reader.onerror = (err) => {
reject(err)
}
reader.readAsArrayBuffer(chunk)
})
}

View file

@ -0,0 +1,20 @@
/**
* uriToBlob resolves a URI to a Blob object. This is used for
* React Native to retrieve a file (identified by a file://
* URI) as a blob.
*/
export default function uriToBlob(uri) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.responseType = 'blob'
xhr.onload = () => {
const blob = xhr.response
resolve(blob)
}
xhr.onerror = (err) => {
reject(err)
}
xhr.open('GET', uri)
xhr.send()
})
}

View file

@ -0,0 +1,71 @@
let hasStorage = false
try {
// Note: localStorage does not exist in the Web Worker's context, so we must use window here.
hasStorage = 'localStorage' in window
// Attempt to store and read entries from the local storage to detect Private
// Mode on Safari on iOS (see #49)
// If the key was not used before, we remove it from local storage again to
// not cause confusion where the entry came from.
const key = 'tusSupport'
const originalValue = localStorage.getItem(key)
localStorage.setItem(key, originalValue)
if (originalValue === null) localStorage.removeItem(key)
} catch (e) {
// If we try to access localStorage inside a sandboxed iframe, a SecurityError
// is thrown. When in private mode on iOS Safari, a QuotaExceededError is
// thrown (see #49)
if (e.code === e.SECURITY_ERR || e.code === e.QUOTA_EXCEEDED_ERR) {
hasStorage = false
} else {
throw e
}
}
export const canStoreURLs = hasStorage
export class WebStorageUrlStorage {
findAllUploads() {
const results = this._findEntries('tus::')
return Promise.resolve(results)
}
findUploadsByFingerprint(fingerprint) {
const results = this._findEntries(`tus::${fingerprint}::`)
return Promise.resolve(results)
}
removeUpload(urlStorageKey) {
localStorage.removeItem(urlStorageKey)
return Promise.resolve()
}
addUpload(fingerprint, upload) {
const id = Math.round(Math.random() * 1e12)
const key = `tus::${fingerprint}::${id}`
localStorage.setItem(key, JSON.stringify(upload))
return Promise.resolve(key)
}
_findEntries(prefix) {
const results = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key.indexOf(prefix) !== 0) continue
try {
const upload = JSON.parse(localStorage.getItem(key))
upload.urlStorageKey = key
results.push(upload)
} catch (_e) {
// The JSON parse error is intentionally ignored here, so a malformed
// entry in the storage cannot prevent an upload.
}
}
return results
}
}

25
frontend/node_modules/tus-js-client/lib/error.js generated vendored Normal file
View file

@ -0,0 +1,25 @@
class DetailedError extends Error {
constructor(message, causingErr = null, req = null, res = null) {
super(message)
this.originalRequest = req
this.originalResponse = res
this.causingError = causingErr
if (causingErr != null) {
message += `, caused by ${causingErr.toString()}`
}
if (req != null) {
const requestId = req.getHeader('X-Request-ID') || 'n/a'
const method = req.getMethod()
const url = req.getURL()
const status = res ? res.getStatus() : 'n/a'
const body = res ? res.getBody() || '' : 'n/a'
message += `, originated from request (method: ${method}, url: ${url}, response code: ${status}, response text: ${body}, request id: ${requestId})`
}
this.message = message
}
}
export default DetailedError

143
frontend/node_modules/tus-js-client/lib/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,143 @@
// Type definitions for tus-js-client
export const isSupported: boolean
export const canStoreURLs: boolean
export const defaultOptions: UploadOptions &
Required<Pick<UploadOptions, 'httpStack' | 'fileReader' | 'urlStorage' | 'fingerprint'>>
// TODO: Consider using { read: () => Promise<{ done: boolean; value?: any; }>; } as type
export class Upload {
constructor(
file: File | Blob | Buffer | Pick<ReadableStreamDefaultReader, 'read'>,
options: UploadOptions,
)
file: File | Blob | Buffer | Pick<ReadableStreamDefaultReader, 'read'>
options: UploadOptions
url: string | null
static terminate(url: string, options?: UploadOptions): Promise<void>
start(): void
abort(shouldTerminate?: boolean): Promise<void>
findPreviousUploads(): Promise<PreviousUpload[]>
resumeFromPreviousUpload(previousUpload: PreviousUpload): void
}
interface UploadOptions {
endpoint?: string | null
uploadUrl?: string | null
metadata?: { [key: string]: string }
metadataForPartialUploads?: { [key: string]: string }
fingerprint?: (file: File, options: UploadOptions) => Promise<string>
uploadSize?: number | null
onProgress?: ((bytesSent: number, bytesTotal: number) => void) | null
onChunkComplete?: ((chunkSize: number, bytesAccepted: number, bytesTotal: number) => void) | null
onSuccess?: ((payload: OnSuccessPayload) => void) | null
onError?: ((error: Error | DetailedError) => void) | null
onShouldRetry?:
| ((error: DetailedError, retryAttempt: number, options: UploadOptions) => boolean)
| null
onUploadUrlAvailable?: (() => void) | null
overridePatchMethod?: boolean
headers?: { [key: string]: string }
addRequestId?: boolean
onBeforeRequest?: (req: HttpRequest) => void | Promise<void>
onAfterResponse?: (req: HttpRequest, res: HttpResponse) => void | Promise<void>
chunkSize?: number
retryDelays?: number[] | null
parallelUploads?: number
parallelUploadBoundaries?: { start: number; end: number }[] | null
storeFingerprintForResuming?: boolean
removeFingerprintOnSuccess?: boolean
uploadLengthDeferred?: boolean
uploadDataDuringCreation?: boolean
urlStorage?: UrlStorage
fileReader?: FileReader
httpStack?: HttpStack
}
interface OnSuccessPayload {
lastResponse: HttpResponse
}
interface UrlStorage {
findAllUploads(): Promise<PreviousUpload[]>
findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]>
removeUpload(urlStorageKey: string): Promise<void>
// Returns the URL storage key, which can be used for removing the upload.
addUpload(fingerprint: string, upload: PreviousUpload): Promise<string>
}
interface PreviousUpload {
size: number | null
metadata: { [key: string]: string }
creationTime: string
urlStorageKey: string
uploadUrl: string | null
parallelUploadUrls: string[] | null
}
interface FileReader {
openFile(input: any, chunkSize: number): Promise<FileSource>
}
interface FileSource {
size: number
slice(start: number, end: number): Promise<SliceResult>
close(): void
}
interface SliceResult {
// Platform-specific data type which must be usable by the HTTP stack as a body.
value: any
done: boolean
}
export class DefaultHttpStack implements HttpStack {
constructor(options: any)
createRequest(method: string, url: string): HttpRequest
getName(): string
}
export interface HttpStack {
createRequest(method: string, url: string): HttpRequest
getName(): string
}
export interface HttpRequest {
getMethod(): string
getURL(): string
setHeader(header: string, value: string): void
getHeader(header: string): string | undefined
setProgressHandler(handler: (bytesSent: number) => void): void
send(body: any): Promise<HttpResponse>
abort(): Promise<void>
// Return an environment specific object, e.g. the XMLHttpRequest object in browsers.
getUnderlyingObject(): any
}
export interface HttpResponse {
getStatus(): number
getHeader(header: string): string | undefined
getBody(): string
// Return an environment specific object, e.g. the XMLHttpRequest object in browsers.
getUnderlyingObject(): any
}
export class DetailedError extends Error {
originalRequest: HttpRequest
originalResponse: HttpResponse | null
causingError: Error | null
}

View file

@ -0,0 +1,93 @@
// This is a test file for ensuring that the type definitions in index.d.ts are
// working correctly. For more details see:
// https://github.com/SamVerschueren/tsd
import { expectType } from 'tsd'
import * as tus from '../'
import type { DetailedError } from '../'
expectType<boolean>(tus.isSupported)
expectType<boolean>(tus.canStoreURLs)
const file = new File(['foo'], 'foo.txt', {
type: 'text/plain',
})
const upload = new tus.Upload(file, {
endpoint: '',
fingerprint: (file: File) => Promise.resolve(file.name),
metadata: {
filename: 'foo.txt',
},
metadataForPartialUploads: {
userId: 'foo123bar',
},
onProgress: (bytesSent: number, bytesTotal: number) => {
const percentage = ((bytesSent / bytesTotal) * 100).toFixed(2)
console.log(bytesSent, bytesTotal, `${percentage}%`)
},
onChunkComplete: (_chunkSize: number, _bytesAccepted: number) => {},
onSuccess: (payload: tus.OnSuccessPayload) => {
console.log('Download from %s complete', upload.url)
console.log('Response header', payload.lastResponse.getHeader('X-Info'))
},
onError: (error: Error | DetailedError) => {
console.error(`Failed because: ${error}`)
},
headers: { TestHeader: 'TestValue' },
chunkSize: 100,
uploadUrl: '',
uploadSize: 50,
overridePatchMethod: true,
retryDelays: [10, 20, 50],
removeFingerprintOnSuccess: true,
parallelUploads: 42,
parallelUploadBoundaries: [
{ start: 0, end: 1 },
{ start: 1, end: 11 },
],
onAfterResponse: (req: tus.HttpRequest, res: tus.HttpResponse) => {
const url = req.getURL()
const value = res.getHeader('X-My-Header')
console.log(`Request for ${url} responded with ${value}`)
},
})
upload.start()
upload.findPreviousUploads().then((uploads: tus.PreviousUpload[]) => {
upload.resumeFromPreviousUpload(uploads[0])
})
upload.abort()
upload.abort(true).then(() => {})
const _upload2 = new tus.Upload(file, {
endpoint: '',
})
// const reader = {
// read: () => Promise.resolve({ done: true, value: '' }),
// };
// const upload3 = new tus.Upload(reader, {
// endpoint: '',
// uploadLengthDeferred: true,
// });
fetch('https://www.example.org')
.then((response) => response.body)
.then((rb) => {
if (rb == null) {
return
}
const reader = rb.getReader()
return new tus.Upload(reader, {
endpoint: '',
uploadLengthDeferred: true,
})
})
tus.Upload.terminate('https://myurl.com', {
endpoint: '',
})

10
frontend/node_modules/tus-js-client/lib/logger.js generated vendored Normal file
View file

@ -0,0 +1,10 @@
let isEnabled = false
export function enableDebugLog() {
isEnabled = true
}
export function log(msg) {
if (!isEnabled) return
console.log(msg)
}

View file

@ -0,0 +1,34 @@
import { ReadStream } from 'fs'
import isStream from 'is-stream'
import BufferSource from './sources/BufferSource.js'
import getFileSource from './sources/FileSource.js'
import StreamSource from './sources/StreamSource.js'
export default class FileReader {
openFile(input, chunkSize) {
if (Buffer.isBuffer(input)) {
return Promise.resolve(new BufferSource(input))
}
if (input instanceof ReadStream && input.path != null) {
return getFileSource(input)
}
if (isStream.readable(input)) {
chunkSize = Number(chunkSize)
if (!Number.isFinite(chunkSize)) {
return Promise.reject(
new Error(
'cannot create source for stream without a finite value for the `chunkSize` option; specify a chunkSize to control the memory consumption',
),
)
}
return Promise.resolve(new StreamSource(input))
}
return Promise.reject(
new Error('source object may only be an instance of Buffer or Readable in this environment'),
)
}
}

View file

@ -0,0 +1,39 @@
import { createHash } from 'crypto'
import * as fs from 'fs'
import * as path from 'path'
/**
* Generate a fingerprint for a file which will be used the store the endpoint
*
* @param {File} file
* @param {Object} options
*/
export default function fingerprint(file, options) {
if (Buffer.isBuffer(file)) {
// create MD5 hash for buffer type
const blockSize = 64 * 1024 // 64kb
const content = file.slice(0, Math.min(blockSize, file.length))
const hash = createHash('md5').update(content).digest('hex')
const ret = ['node-buffer', hash, file.length, options.endpoint].join('-')
return Promise.resolve(ret)
}
if (file instanceof fs.ReadStream && file.path != null) {
return new Promise((resolve, reject) => {
const name = path.resolve(file.path)
fs.stat(file.path, (err, info) => {
if (err) {
reject(err)
return
}
const ret = ['node-file', name, info.size, info.mtime.getTime(), options.endpoint].join('-')
resolve(ret)
})
})
}
// fingerprint cannot be computed for file input type
return Promise.resolve(null)
}

View file

@ -0,0 +1,212 @@
// The url.parse method is superseeded by the url.URL constructor,
// but it is still included in Node.js
import * as http from 'http'
import * as https from 'https'
import { Readable, Transform } from 'stream'
import { parse } from 'url'
import throttle from 'lodash.throttle'
export default class NodeHttpStack {
constructor(requestOptions = {}) {
this._requestOptions = requestOptions
}
createRequest(method, url) {
return new Request(method, url, this._requestOptions)
}
getName() {
return 'NodeHttpStack'
}
}
class Request {
constructor(method, url, options) {
this._method = method
this._url = url
this._headers = {}
this._request = null
this._progressHandler = () => {}
this._requestOptions = options || {}
}
getMethod() {
return this._method
}
getURL() {
return this._url
}
setHeader(header, value) {
this._headers[header] = value
}
getHeader(header) {
return this._headers[header]
}
setProgressHandler(progressHandler) {
this._progressHandler = progressHandler
}
send(body = null) {
return new Promise((resolve, reject) => {
const options = {
...parse(this._url),
...this._requestOptions,
method: this._method,
headers: {
...(this._requestOptions.headers || {}),
...this._headers,
},
}
if (body?.size) {
options.headers['Content-Length'] = body.size
}
const httpModule = options.protocol === 'https:' ? https : http
this._request = httpModule.request(options)
const req = this._request
req.on('response', (res) => {
const resChunks = []
res.on('data', (data) => {
resChunks.push(data)
})
res.on('end', () => {
const responseText = Buffer.concat(resChunks).toString('utf8')
resolve(new Response(res, responseText))
})
})
req.on('error', (err) => {
reject(err)
})
if (body instanceof Readable) {
// Readable stream are piped through a PassThrough instance, which
// counts the number of bytes passed through. This is used, for example,
// when an fs.ReadStream is provided to tus-js-client.
body.pipe(new ProgressEmitter(this._progressHandler)).pipe(req)
} else if (body instanceof Uint8Array) {
// For Buffers and Uint8Arrays (in Node.js all buffers are instances of Uint8Array),
// we write chunks of the buffer to the stream and use that to track the progress.
// This is used when either a Buffer or a normal readable stream is provided
// to tus-js-client.
writeBufferToStreamWithProgress(req, body, this._progressHandler)
} else {
req.end(body)
}
})
}
abort() {
if (this._request !== null) this._request.abort()
return Promise.resolve()
}
getUnderlyingObject() {
return this._request
}
}
class Response {
constructor(res, body) {
this._response = res
this._body = body
}
getStatus() {
return this._response.statusCode
}
getHeader(header) {
return this._response.headers[header.toLowerCase()]
}
getBody() {
return this._body
}
getUnderlyingObject() {
return this._response
}
}
// ProgressEmitter is a simple PassThrough-style transform stream which keeps
// track of the number of bytes which have been piped through it and will
// invoke the `onprogress` function whenever new number are available.
class ProgressEmitter extends Transform {
constructor(onprogress) {
super()
// The _onprogress property will be invoked, whenever a chunk is piped
// through this transformer. Since chunks are usually quite small (64kb),
// these calls can occur frequently, especially when you have a good
// connection to the remote server. Therefore, we are throtteling them to
// prevent accessive function calls.
this._onprogress = throttle(onprogress, 100, {
leading: true,
trailing: false,
})
this._position = 0
}
_transform(chunk, _encoding, callback) {
this._position += chunk.length
this._onprogress(this._position)
callback(null, chunk)
}
}
// writeBufferToStreamWithProgress writes chunks from `source` (either a
// Buffer or Uint8Array) to the readable stream `stream`.
// The size of the chunk depends on the stream's highWaterMark to fill the
// stream's internal buffer as best as possible.
// If the internal buffer is full, the callback `onprogress` will be invoked
// to notify about the write progress. Writing will be resumed once the internal
// buffer is empty, as indicated by the emitted `drain` event.
// See https://nodejs.org/docs/latest/api/stream.html#buffering for more details
// on the buffering behavior of streams.
const writeBufferToStreamWithProgress = (stream, source, onprogress) => {
onprogress = throttle(onprogress, 100, {
leading: true,
trailing: false,
})
let offset = 0
function writeNextChunk() {
// Take at most the amount of bytes from highWaterMark. This should fill the streams
// internal buffer already.
const chunkSize = Math.min(stream.writableHighWaterMark, source.length - offset)
// Note: We use subarray instead of slice because it works without copying data for
// Buffers and Uint8Arrays.
const chunk = source.subarray(offset, offset + chunkSize)
offset += chunk.length
// `write` returns true if the internal buffer is not full and we should write more.
// If the stream is destroyed because the request is aborted, it will return false
// and no 'drain' event is emitted, so won't continue writing data.
const canContinue = stream.write(chunk)
if (!canContinue) {
// If the buffer is full, wait for the 'drain' event to write more data.
stream.once('drain', writeNextChunk)
onprogress(offset)
} else if (offset < source.length) {
// If there's still data to write and the buffer is not full, write next chunk.
writeNextChunk()
} else {
// If all data has been written, close the stream if needed, and emit a 'finish' event.
stream.end()
}
}
// Start writing the first chunk.
writeNextChunk()
}

47
frontend/node_modules/tus-js-client/lib/node/index.js generated vendored Normal file
View file

@ -0,0 +1,47 @@
import DetailedError from '../error.js'
import { enableDebugLog } from '../logger.js'
import NoopUrlStorage from '../noopUrlStorage.js'
import BaseUpload from '../upload.js'
import FileReader from './fileReader.js'
import fingerprint from './fileSignature.js'
import DefaultHttpStack from './httpStack.js'
import StreamSource from './sources/StreamSource.js'
import { FileUrlStorage, canStoreURLs } from './urlStorage.js'
const defaultOptions = {
...BaseUpload.defaultOptions,
httpStack: new DefaultHttpStack(),
fileReader: new FileReader(),
urlStorage: new NoopUrlStorage(),
fingerprint,
}
class Upload extends BaseUpload {
constructor(file = null, options = {}) {
options = { ...defaultOptions, ...options }
super(file, options)
}
static terminate(url, options = {}) {
options = { ...defaultOptions, ...options }
return BaseUpload.terminate(url, options)
}
}
// The Node.js environment does not have restrictions which may cause
// tus-js-client not to function.
const isSupported = true
export {
Upload,
defaultOptions,
isSupported,
// Make FileUrlStorage module available as it will not be set by default.
FileUrlStorage,
canStoreURLs,
enableDebugLog,
DefaultHttpStack,
DetailedError,
StreamSource,
}

View file

@ -0,0 +1,15 @@
export default class BufferSource {
constructor(buffer) {
this._buffer = buffer
this.size = buffer.length
}
slice(start, end) {
const value = this._buffer.slice(start, end)
value.size = value.length
const done = end >= this.size
return Promise.resolve({ value, done })
}
close() {}
}

View file

@ -0,0 +1,57 @@
import { createReadStream, promises as fsPromises } from 'fs'
export default async function getFileSource(stream) {
const path = stream.path.toString()
const { size } = await fsPromises.stat(path)
// The fs.ReadStream might be configured to not read from the beginning
// to the end, but instead from a slice in between. In this case, we consider
// that range to indicate the actual uploadable size.
// This happens, for example, if a fs.ReadStream is used with the `parallelUploads`
// option. There, the ReadStream is sliced into multiple ReadStreams to fit the number
// of number of `parallelUploads`. Each ReadStream has `start` and `end` set.
// Note: `stream.end` is Infinity by default, so we need the check `isFinite`.
// Note: `stream.end` is treated inclusively, so we need to add 1 here.
// See the comment in slice() for more context.
const start = stream.start ?? 0
const end = Number.isFinite(stream.end) ? stream.end + 1 : size
const actualSize = end - start
return new FileSource(stream, path, actualSize)
}
class FileSource {
constructor(stream, path, size) {
this._stream = stream
this._path = path
this.size = size
}
slice(start, end) {
// The fs.ReadStream might be configured to not read from the beginning,
// but instead start at a different offset. The start value from the caller
// does not include the offset, so we need to add this offset to our range later.
// This happens, for example, if a fs.ReadStream is used with the `parallelUploads`
// option. First, the ReadStream is sliced into multiple ReadStreams to fit the number
// of number of `parallelUploads`. Each ReadStream has `start` set.
const offset = this._stream.start ?? 0
const stream = createReadStream(this._path, {
start: offset + start,
// The `end` option for createReadStream is treated inclusively
// (see https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options).
// However, the Buffer#slice(start, end) and also our Source#slice(start, end)
// method treat the end range exclusively, so we have to subtract 1.
// This prevents an off-by-one error when reporting upload progress.
end: offset + end - 1,
autoClose: true,
})
stream.size = Math.min(end - start, this.size)
const done = stream.size >= this.size
return Promise.resolve({ value: stream, done })
}
close() {
this._stream.destroy()
}
}

View file

@ -0,0 +1,123 @@
/**
* readChunk reads a chunk with the given size from the given
* stream. It will wait until enough data is available to satisfy
* the size requirement before resolving.
* Only if the stream ends, the function may resolve with a buffer
* smaller than the size argument.
* Note that we rely on the stream behaving as Node.js documents:
* https://nodejs.org/api/stream.html#readablereadsize
*/
async function readChunk(stream, size) {
return new Promise((resolve, reject) => {
const onError = (err) => {
stream.off('readable', onReadable)
reject(err)
}
const onReadable = () => {
// TODO: Node requires size to be less than 1GB. Add a validation for that
const chunk = stream.read(size)
if (chunk !== null) {
stream.off('error', onError)
stream.off('readable', onReadable)
resolve(chunk)
}
}
stream.once('error', onError)
stream.on('readable', onReadable)
})
}
/**
* StreamSource provides an interface to obtain slices of a Readable stream for
* various ranges.
* It will buffer read data, to allow for following pattern:
* - Call slice(startA, endA) will buffer the data of the requested range
* - Call slice(startB, endB) will return data from the buffer if startA <= startB <= endA.
* If endB > endA, it will also consume new data from the stream.
* Note that it is forbidden to call with startB < startA or startB > endA. In other words,
* the slice calls cannot seek back and must not skip data from the stream.
*/
export default class StreamSource {
constructor(stream) {
this._stream = stream
// Setting the size to null indicates that we have no calculation available
// for how much data this stream will emit requiring the user to specify
// it manually (see the `uploadSize` option).
this.size = null
this._buf = Buffer.alloc(0)
this._bufPos = 0
this._ended = false
this._error = null
stream.pause()
stream.on('end', () => {
this._ended = true
})
stream.on('error', (err) => {
this._error = err
})
}
async slice(start, end) {
// Fail fast if the caller requests a proportion of the data which is not
// available any more.
if (start < this._bufPos) {
throw new Error('cannot slice from position which we already seeked away')
}
if (start > this._bufPos + this._buf.length) {
throw new Error('slice start is outside of buffer (currently not implemented)')
}
if (this._error) {
throw this._error
}
let returnBuffer
// Always attempt to drain the buffer first, even if this means that we
// return less data than the caller requested.
if (start < this._bufPos + this._buf.length) {
const bufStart = start - this._bufPos
const bufEnd = Math.min(this._buf.length, end - this._bufPos)
returnBuffer = this._buf.slice(bufStart, bufEnd)
} else {
returnBuffer = Buffer.alloc(0)
}
// If the stream has ended already, read calls would not finish, so return early here.
if (this._ended) {
returnBuffer.size = returnBuffer.length
return { value: returnBuffer, done: true }
}
// If we could not satisfy the slice request from the buffer only, read more data from
// the stream and add it to the buffer.
const requestedSize = end - start
if (requestedSize > returnBuffer.length) {
// Note: We assume that the stream returns not more than the requested size.
const newChunk = await readChunk(this._stream, requestedSize - returnBuffer.length)
// Append the new chunk to the buffer
returnBuffer = Buffer.concat([returnBuffer, newChunk])
}
// Important: Store the read data, so consecutive slice calls can access the same data.
this._buf = returnBuffer
this._bufPos = start
returnBuffer.size = returnBuffer.length
return { value: returnBuffer, done: this._ended }
}
close() {
this._stream.destroy()
}
}

View file

@ -0,0 +1,156 @@
import { readFile, writeFile } from 'fs'
import combineErrors from 'combine-errors'
import * as lockfile from 'proper-lockfile'
export const canStoreURLs = true
export class FileUrlStorage {
constructor(filePath) {
this.path = filePath
}
findAllUploads() {
return new Promise((resolve, reject) => {
this._getItems('tus::', (err, results) => {
if (err) reject(err)
else resolve(results)
})
})
}
findUploadsByFingerprint(fingerprint) {
return new Promise((resolve, reject) => {
this._getItems(`tus::${fingerprint}`, (err, results) => {
if (err) reject(err)
else resolve(results)
})
})
}
removeUpload(urlStorageKey) {
return new Promise((resolve, reject) => {
this._removeItem(urlStorageKey, (err) => {
if (err) reject(err)
else resolve()
})
})
}
addUpload(fingerprint, upload) {
const id = Math.round(Math.random() * 1e12)
const key = `tus::${fingerprint}::${id}`
return new Promise((resolve, reject) => {
this._setItem(key, upload, (err) => {
if (err) reject(err)
else resolve(key)
})
})
}
_setItem(key, value, cb) {
lockfile
.lock(this.path, this._lockfileOptions())
.then((release) => {
cb = this._releaseAndCb(release, cb)
this._getData((err, data) => {
if (err) {
cb(err)
return
}
data[key] = value
this._writeData(data, (err2) => cb(err2))
})
})
.catch(cb)
}
_getItems(prefix, cb) {
this._getData((err, data) => {
if (err) {
cb(err)
return
}
const results = Object.keys(data)
.filter((key) => key.startsWith(prefix))
.map((key) => {
const obj = data[key]
obj.urlStorageKey = key
return obj
})
cb(null, results)
})
}
_removeItem(key, cb) {
lockfile
.lock(this.path, this._lockfileOptions())
.then((release) => {
cb = this._releaseAndCb(release, cb)
this._getData((err, data) => {
if (err) {
cb(err)
return
}
delete data[key]
this._writeData(data, (err2) => cb(err2))
})
})
.catch(cb)
}
_lockfileOptions() {
return {
realpath: false,
retries: {
retries: 5,
minTimeout: 20,
},
}
}
_releaseAndCb(release, cb) {
return (err) => {
if (err) {
release()
.then(() => cb(err))
.catch((releaseErr) => cb(combineErrors([err, releaseErr])))
return
}
release().then(cb).catch(cb)
}
}
_writeData(data, cb) {
const opts = {
encoding: 'utf8',
mode: 0o660,
flag: 'w',
}
writeFile(this.path, JSON.stringify(data), opts, (err) => cb(err))
}
_getData(cb) {
readFile(this.path, 'utf8', (err, data) => {
if (err) {
// return empty data if file does not exist
if (err.code === 'ENOENT') cb(null, {})
else cb(err)
return
}
try {
data = !data.trim().length ? {} : JSON.parse(data)
} catch (error) {
cb(error)
return
}
cb(null, data)
})
}
}

View file

@ -0,0 +1,17 @@
export default class NoopUrlStorage {
listAllUploads() {
return Promise.resolve([])
}
findUploadsByFingerprint(_fingerprint) {
return Promise.resolve([])
}
removeUpload(_urlStorageKey) {
return Promise.resolve()
}
addUpload(_fingerprint, _upload) {
return Promise.resolve(null)
}
}

1120
frontend/node_modules/tus-js-client/lib/upload.js generated vendored Normal file

File diff suppressed because it is too large Load diff

19
frontend/node_modules/tus-js-client/lib/uuid.js generated vendored Normal file
View file

@ -0,0 +1,19 @@
/**
* Generate a UUID v4 based on random numbers. We intentioanlly use the less
* secure Math.random function here since the more secure crypto.getRandomNumbers
* is not available on all platforms.
* This is not a problem for us since we use the UUID only for generating a
* request ID, so we can correlate server logs to client errors.
*
* This function is taken from following site:
* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*
* @return {string} The generate UUID
*/
export default function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}