Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions lib/browser/BrowserFileReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
supportedTypes as supportedBaseTypes,
} from '../commonFileReader.js'
import type { FileReader, FileSource, UploadInput } from '../options.js'
import { BlobFileSource } from '../sources/BlobFileSource.js'
import { ReactNativeFileSource } from "../sources/ReactNativeFileSource.js";

export class BrowserFileReader implements FileReader {
async openFile(input: UploadInput, chunkSize: number): Promise<FileSource> {
Expand All @@ -20,8 +20,7 @@ export class BrowserFileReader implements FileReader {
}

try {
const blob = await uriToBlob(input.uri)
return new BlobFileSource(blob)
return new ReactNativeFileSource(input)
} catch (err) {
throw new Error(
`tus: cannot fetch \`file.uri\` as Blob, make sure the uri is correct and accessible. ${err}`,
Expand Down
42 changes: 0 additions & 42 deletions lib/browser/fileSignature.ts

This file was deleted.

5 changes: 2 additions & 3 deletions lib/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import { DetailedError } from '../DetailedError.js'
import { NoopUrlStorage } from '../NoopUrlStorage.js'
import { enableDebugLog } from '../logger.js'
import type { UploadInput, UploadOptions } from '../options.js'
import { BaseUpload, defaultOptions as baseDefaultOptions, terminate } from '../upload.js'
import { BaseUpload, defaultOptions as baseDefaultOptions, noOpFingerprint, terminate } from '../upload.js'

import { BrowserFileReader } from './BrowserFileReader.js'
import { XHRHttpStack as DefaultHttpStack } from './XHRHttpStack.js'
import { fingerprint } from './fileSignature.js'
import { WebStorageUrlStorage, canStoreURLs } from './urlStorage.js'

const defaultOptions = {
...baseDefaultOptions,
httpStack: new DefaultHttpStack(),
fileReader: new BrowserFileReader(),
urlStorage: canStoreURLs ? new WebStorageUrlStorage() : new NoopUrlStorage(),
fingerprint,
fingerprint: noOpFingerprint
}

class Upload extends BaseUpload {
Expand Down
30 changes: 0 additions & 30 deletions lib/node/fileSignature.ts

This file was deleted.

5 changes: 2 additions & 3 deletions lib/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import { DetailedError } from '../DetailedError.js'
import { NoopUrlStorage } from '../NoopUrlStorage.js'
import { enableDebugLog } from '../logger.js'
import type { UploadInput, UploadOptions } from '../options.js'
import { BaseUpload, defaultOptions as baseDefaultOptions, terminate } from '../upload.js'
import { BaseUpload, defaultOptions as baseDefaultOptions, noOpFingerprint, terminate } from '../upload.js'

import { canStoreURLs } from './FileUrlStorage.js'
import { NodeFileReader } from './NodeFileReader.js'
import { NodeHttpStack as DefaultHttpStack } from './NodeHttpStack.js'
import { fingerprint } from './fileSignature.js'

const defaultOptions = {
...baseDefaultOptions,
httpStack: new DefaultHttpStack(),
fileReader: new NodeFileReader(),
urlStorage: new NoopUrlStorage(),
fingerprint,
fingerprint: noOpFingerprint
}

class Upload extends BaseUpload {
Expand Down
13 changes: 10 additions & 3 deletions lib/node/sources/NodeStreamFileSource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Readable } from 'node:stream'
import type { FileSource } from '../../options.js'
import { Readable, Transform } from 'node:stream'
import type { FileSource, UploadOptions } from '../../options.js'

/**
* readChunk reads a chunk with the given size from the given
Expand Down Expand Up @@ -74,9 +74,16 @@ export class NodeStreamFileSource implements FileSource {
})
}

// TODO: Implement fingerprint calculation for NodeStreamFileSource.
// This should likely involve reading the stream and creating a hash/checksum
// while preserving the stream's content for later use.
fingerprint(options: UploadOptions): Promise<string | null> {
return Promise.resolve(null);
}

async slice(start: number, end: number) {
// Fail fast if the caller requests a proportion of the data which is not
// available any more.
// available anymore.
if (start < this._bufPos) {
throw new Error('cannot slice from position which we already seeked away')
}
Expand Down
8 changes: 6 additions & 2 deletions lib/node/sources/PathFileSource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ReadStream, createReadStream, promises as fsPromises } from 'node:fs'
import type { FileSource, PathReference } from '../../options.js'
import { createReadStream, promises as fsPromises, type ReadStream } from 'node:fs'
import type { FileSource, PathReference, UploadOptions } from '../../options.js'

export async function getFileSourceFromPath(file: PathReference): Promise<PathFileSource> {
const path = file.path.toString()
Expand Down Expand Up @@ -34,6 +34,10 @@ export class PathFileSource implements FileSource {
this.size = size
}

fingerprint(options: UploadOptions): Promise<string | null> {
return Promise.resolve(["tus-path-file-source", this.size, this._path, options.endpoint].join('-'))
}

slice(start: number, end: number) {
// TODO: Does this create multiple file descriptors? Can we reduce this by
// using a file handle instead?
Expand Down
5 changes: 3 additions & 2 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ReactNativeFile {
/**
* PathReference is a reference to a file on disk. Currently, it's only supported
* in Node.js. It can be supplied as a normal object or as an instance of `fs.ReadStream`,
* which also statisfies this interface.
* which also satisfies this interface.
*
* Optionally, a start and/or end position can be defined to define a range of bytes from
* the file that should be uploaded instead of the entire file. Both start and end are
Expand Down Expand Up @@ -54,7 +54,7 @@ export interface UploadOptions {
uploadUrl?: string
metadata: { [key: string]: string }
metadataForPartialUploads: UploadOptions['metadata']
fingerprint: (file: UploadInput, options: UploadOptions) => Promise<string | null>
fingerprint: (file: UploadInput, options: UploadOptions, sourceFingerprint: string | null) => Promise<string | null>
uploadSize?: number

onProgress?: (bytesSent: number, bytesTotal: number | null) => void
Expand Down Expand Up @@ -117,6 +117,7 @@ export interface FileSource {
size: number | null
slice(start: number, end: number): Promise<SliceResult>
close(): void
fingerprint(options: UploadOptions): Promise<string | null>
}

// TODO: Allow Web Streams' ReadableStream as well
Expand Down
13 changes: 10 additions & 3 deletions lib/sources/ArrayBufferViewFileSource.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { FileSource, SliceResult } from '../options.js'
import type { FileSource, SliceResult, UploadOptions } from '../options.js'

/**
* ArrayBufferViewFileSource implements FileSource for ArrayBufferView instances
* (e.g. TypedArry or DataView).
* (e.g., TypedArray or DataView).
*
* Note that the underlying ArrayBuffer should not change once passed to tus-js-client
* or it will lead to weird behavior.
*/
export class ArrayBufferViewFileSource implements FileSource {
private _view: ArrayBufferView
private readonly _view: ArrayBufferView

size: number

Expand All @@ -17,6 +17,13 @@ export class ArrayBufferViewFileSource implements FileSource {
this.size = view.byteLength
}

async fingerprint(options: UploadOptions): Promise<string | null> {
const buffer = this._view.buffer.slice(this._view.byteOffset, this._view.byteOffset + this._view.byteLength);
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashContent = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
return Promise.resolve([hashContent, options.endpoint].join('-'));
}

slice(start: number, end: number): Promise<SliceResult> {
const buffer = this._view.buffer
const startInBuffer = this._view.byteOffset + start
Expand Down
17 changes: 15 additions & 2 deletions lib/sources/BlobFileSource.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { isCordova } from '../cordova/isCordova.js'
import { readAsByteArray } from '../cordova/readAsByteArray.js'
import type { FileSource, SliceResult } from '../options.js'
import type { FileSource, SliceResult, UploadOptions } from '../options.js'

/**
* BlobFileSource implements FileSource for Blobs (and therefore also for File instances).
*/
export class BlobFileSource implements FileSource {
private _file: Blob
private readonly _file: Blob

size: number

Expand All @@ -15,6 +15,19 @@ export class BlobFileSource implements FileSource {
this.size = file.size
}

fingerprint(options: UploadOptions): Promise<string | null> {
let name, lastModified;
if (this._file instanceof File) {
name = this._file.name;
lastModified = this._file.lastModified;
} else {
name = 'blob';
lastModified = 0;
}

return Promise.resolve(['tus-br', name, this._file.type, this._file.size, lastModified, options.endpoint].join('-'));
}

async slice(start: number, end: number): Promise<SliceResult> {
// TODO: This looks fishy. We should test how this actually works in Cordova
// and consider moving this into the lib/cordova/ directory.
Expand Down
51 changes: 51 additions & 0 deletions lib/sources/ReactNativeFileSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FileSource, ReactNativeFile, SliceResult, UploadOptions } from "../options.js";

export class ReactNativeFileSource implements FileSource {
private readonly _file: ReactNativeFile
size: number;

constructor(file: ReactNativeFile) {
this._file = file
this.size = Number(file.size)
}

fingerprint(options: UploadOptions): Promise<string | null> {
return Promise.resolve(this.reactNativeFingerprint(this._file, options))
}

// TODO: Implement the slice method to read file content from start to end positions
// The implementation should:
// 1. Read the file content from the ReactNative file URI
// 2. Return the sliced content as value
// 3. Calculate proper size and done values
async slice(start: number, end: number): Promise<SliceResult> {
let value = null, size = null, done = true;

return { value, size, done }
}

close(): void {
// Nothing to do here since we don't need to release any resources.
}

private reactNativeFingerprint(file: ReactNativeFile, options: UploadOptions): string {
const exifHash = file.exif ? this.hashCode(JSON.stringify(file.exif)) : 'noexif'
return ['tus-rn', file.name || 'noname', file.size || 'nosize', exifHash, options.endpoint].join(
'/',
)
}

private hashCode(str: string): number {
// 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
}
}
9 changes: 6 additions & 3 deletions lib/sources/WebStreamFileSource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FileSource, SliceResult } from '../options.js'
import type {FileSource, SliceResult, UploadOptions} from '../options.js'

function len(blobOrArray: WebStreamFileSource['_buffer']): number {
if (blobOrArray === undefined) return 0
Expand Down Expand Up @@ -28,7 +28,7 @@ function concat<T extends WebStreamFileSource['_buffer']>(a: T, b: T): T {
*/
// TODO: Can we share code with NodeStreamFileSource?
export class WebStreamFileSource implements FileSource {
private _reader: ReadableStreamDefaultReader<Uint8Array>
private readonly _reader: ReadableStreamDefaultReader<Uint8Array>

private _buffer: Blob | Uint8Array | undefined

Expand All @@ -50,10 +50,13 @@ export class WebStreamFileSource implements FileSource {
'Readable stream is already locked to reader. tus-js-client cannot obtain a new reader.',
)
}

this._reader = stream.getReader()
}

fingerprint(options: UploadOptions): Promise<string | null> {
return Promise.resolve(null);
}

async slice(start: number, end: number): Promise<SliceResult> {
if (start < this._bufferOffset) {
throw new Error("Requested data is before the reader's current offset")
Expand Down
Loading
Loading