@travetto/asset
Advanced tools
Comparing version 4.1.3 to 5.0.0-rc.0
export * from './src/types'; | ||
export * from './src/service'; | ||
export * from './src/naming'; | ||
export * from './src/util'; | ||
export * from './src/util'; | ||
export * from './src/response'; |
{ | ||
"name": "@travetto/asset", | ||
"version": "4.1.3", | ||
"version": "5.0.0-rc.0", | ||
"description": "Modular library for storing and retrieving binary assets", | ||
@@ -28,4 +28,4 @@ "keywords": [ | ||
"dependencies": { | ||
"@travetto/di": "^4.1.1", | ||
"@travetto/model": "^4.1.3", | ||
"@travetto/di": "^5.0.0-rc.0", | ||
"@travetto/model": "^5.0.0-rc.0", | ||
"@types/mime": "^3.0.4", | ||
@@ -36,3 +36,3 @@ "file-type": "^16.5.4", | ||
"peerDependencies": { | ||
"@travetto/test": "^4.1.1" | ||
"@travetto/test": "^5.0.0-rc.0" | ||
}, | ||
@@ -39,0 +39,0 @@ "peerDependenciesMeta": { |
@@ -27,3 +27,3 @@ <!-- This file was generated by @travetto/doc and should not be modified directly --> | ||
Currently, the following are packages that provide [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3) support: | ||
* [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") - [FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#L50), [MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L53) | ||
* [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") - [FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#L50), [MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L54) | ||
* [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.") | ||
@@ -56,3 +56,3 @@ * [S3 Model Support](https://github.com/travetto/travetto/tree/main/module/model-s3#readme "S3 backing for the travetto model module.") | ||
Reading of and writing assets uses the [AssetService](https://github.com/travetto/travetto/tree/main/module/asset/src/service.ts#L18). Below you can see an example dealing with a user's profile image. | ||
Reading of and writing assets uses the [AssetService](https://github.com/travetto/travetto/tree/main/module/asset/src/service.ts#L20). Below you can see an example dealing with a user's profile image. | ||
@@ -90,3 +90,3 @@ **Code: User Profile Images** | ||
The underlying contract for a [AssetNamingStrategy](https://github.com/travetto/travetto/tree/main/module/asset/src/naming.ts#L9) looks like: | ||
The underlying contract for a [AssetNamingStrategy](https://github.com/travetto/travetto/tree/main/module/asset/src/naming.ts#L8) looks like: | ||
@@ -126,13 +126,2 @@ **Code: AssetNamingStrategy** | ||
} | ||
/** | ||
* An asset response | ||
*/ | ||
export interface AssetResponse extends StreamMeta { | ||
stream(): Readable; | ||
/** | ||
* Response byte range, inclusive | ||
*/ | ||
range?: [start: number, end: number]; | ||
} | ||
``` | ||
@@ -139,0 +128,0 @@ |
@@ -1,4 +0,3 @@ | ||
import { getExtension } from 'mime'; | ||
import { StreamMeta } from '@travetto/model'; | ||
import { AssetUtil } from './util'; | ||
@@ -38,6 +37,6 @@ /** | ||
resolve(asset: StreamMeta): string { | ||
let ext = ''; | ||
let ext: string | undefined = ''; | ||
if (asset.contentType) { | ||
ext = getExtension(asset.contentType)!; | ||
ext = AssetUtil.getExtension(asset.contentType); | ||
} else if (asset.filename) { | ||
@@ -44,0 +43,0 @@ const dot = asset.filename.indexOf('.'); |
@@ -1,9 +0,11 @@ | ||
import { PassThrough } from 'node:stream'; | ||
import { PassThrough, Readable } from 'node:stream'; | ||
import { Inject, Injectable } from '@travetto/di'; | ||
import { ModelStreamSupport, ExistsError, NotFoundError, StreamMeta } from '@travetto/model'; | ||
import { StreamUtil } from '@travetto/base'; | ||
import { ModelStreamSupport, ExistsError, NotFoundError, StreamMeta, StreamRange } from '@travetto/model'; | ||
import { enforceRange } from '@travetto/model/src/internal/service/stream'; | ||
import { Asset, AssetResponse } from './types'; | ||
import { Asset } from './types'; | ||
import { AssetNamingStrategy, SimpleNamingStrategy } from './naming'; | ||
import { AssetUtil } from './util'; | ||
import { StreamResponse } from './response'; | ||
@@ -50,2 +52,17 @@ export const AssetModelⲐ = Symbol.for('@travetto/asset:model'); | ||
* | ||
* @param blob The blob to store | ||
* @param meta The optional metadata for the blob | ||
* @param overwriteIfFound Overwrite the asset if found | ||
* @param strategy The naming strategy to use, defaults to the service's strategy if not provided | ||
*/ | ||
async upsertBlob(blob: Blob, meta: Partial<StreamMeta> = {}, overwriteIfFound = true, strategy?: AssetNamingStrategy): Promise<{ asset: Asset, location: string }> { | ||
const asset = await AssetUtil.blobToAsset(blob, meta); | ||
const location = await this.upsert(asset, overwriteIfFound, strategy); | ||
return { asset, location }; | ||
} | ||
/** | ||
* Stores an asset with the optional ability to overwrite if the file is already found. If not | ||
* overwriting and file exists, an error will be thrown. | ||
* | ||
* @param asset The asset to store | ||
@@ -75,3 +92,11 @@ * @param overwriteIfFound Overwrite the asset if found | ||
const stream = await StreamUtil.toStream(source); | ||
let stream: Readable; | ||
if (typeof source === 'string') { | ||
stream = Readable.from(source, { encoding: source.endsWith('=') ? 'base64' : 'utf8' }); | ||
} else if (Buffer.isBuffer(source)) { | ||
stream = Readable.from(source); | ||
} else { | ||
stream = source; | ||
} | ||
await this.#store.upsertStream(location, stream, asset); | ||
@@ -88,15 +113,13 @@ return location; | ||
*/ | ||
async get(location: string, start?: number, end?: number): Promise<AssetResponse> { | ||
const info = await this.describe(location); | ||
async get(location: string, range?: StreamRange): Promise<StreamResponse> { | ||
const meta = await this.describe(location); | ||
if (range) { | ||
range = enforceRange(range, meta.size); | ||
} | ||
const stream = new PassThrough(); | ||
const extra: Partial<AssetResponse> = {}; | ||
let load: () => void; | ||
if (start === undefined) { | ||
load = (): void => { this.#store.getStream(location).then(v => v.pipe(stream)); }; | ||
} else { | ||
extra.range = StreamUtil.enforceRange(start, end, info.size); | ||
load = (): void => { this.#store.getStreamPartial(location, start, end).then(v => v.stream.pipe(stream)); }; | ||
} | ||
return { stream: () => (load(), stream), ...info, ...extra }; | ||
const load = (): void => { this.#store.getStream(location, range).then(v => v.pipe(stream)); }; | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
return new StreamResponse(() => (load(), stream), meta, range as Required<StreamRange>); | ||
} | ||
} |
@@ -13,13 +13,2 @@ import { Readable } from 'node:stream'; | ||
localFile?: string; | ||
} | ||
/** | ||
* An asset response | ||
*/ | ||
export interface AssetResponse extends StreamMeta { | ||
stream(): Readable; | ||
/** | ||
* Response byte range, inclusive | ||
*/ | ||
range?: [start: number, end: number]; | ||
} |
@@ -6,8 +6,7 @@ import fs from 'node:fs/promises'; | ||
import crypto from 'node:crypto'; | ||
import path from 'node:path'; | ||
import { getExtension, getType } from 'mime'; | ||
import { path } from '@travetto/manifest'; | ||
import { StreamMeta } from '@travetto/model'; | ||
import { AppError } from '@travetto/base'; | ||
@@ -22,7 +21,11 @@ import { Asset } from './types'; | ||
/** | ||
* Compute hash from a file location on disk | ||
* Compute hash from a file location on disk or a blob | ||
*/ | ||
static async hashFile(pth: string): Promise<string> { | ||
static async computeHash(input: string | Blob | Buffer): Promise<string> { | ||
const hasher = crypto.createHash('sha256').setEncoding('hex'); | ||
const str = createReadStream(pth); | ||
const str = typeof input === 'string' ? | ||
createReadStream(input) : | ||
Buffer.isBuffer(input) ? | ||
Readable.from(input) : | ||
Readable.fromWeb(input.stream()); | ||
await pipeline(str, hasher); | ||
@@ -33,7 +36,7 @@ return hasher.read().toString(); | ||
/** | ||
* Read a chunk from a file, primarily used for mime detection | ||
* Read a chunk from a file | ||
*/ | ||
static async readChunk(input: string | Readable | Buffer, bytes: number): Promise<Buffer> { | ||
if (Buffer.isBuffer(input)) { | ||
return input; | ||
return input.subarray(0, bytes); | ||
} else if (typeof input === 'string') { | ||
@@ -58,3 +61,3 @@ const fd = await fs.open(input, 'r'); | ||
} | ||
return Buffer.concat(chunks); | ||
return Buffer.concat(chunks).subarray(0, bytes); | ||
} | ||
@@ -64,3 +67,3 @@ } | ||
/** | ||
* Detect file type from location on disk | ||
* Detect file type | ||
*/ | ||
@@ -83,3 +86,3 @@ static async detectFileType(input: string | Buffer | Readable): Promise<{ ext: string, mime: string } | undefined> { | ||
const type = await this.resolveFileType(filePath); | ||
const ext = getExtension(type); | ||
const ext = this.getExtension(type); | ||
const baseName = path.basename(filePath, path.extname(filePath)); | ||
@@ -97,2 +100,10 @@ const newFile = `${baseName}.${ext}`; | ||
/** | ||
* Get extension for a given content type | ||
* @param contentType | ||
*/ | ||
static getExtension(contentType: string): string | undefined { | ||
return getExtension(contentType)!; | ||
} | ||
/** | ||
* Read content type from location on disk | ||
@@ -117,3 +128,3 @@ */ | ||
const hash = metadata.hash ?? await this.hashFile(file); | ||
const hash = metadata.hash ?? await this.computeHash(file); | ||
const size = metadata.size ?? (await fs.stat(file)).size; | ||
@@ -127,3 +138,3 @@ const contentType = metadata.contentType ?? await this.resolveFileType(file); | ||
if (!extName) { | ||
const ext = getExtension(contentType); | ||
const ext = this.getExtension(contentType); | ||
if (ext) { | ||
@@ -139,2 +150,5 @@ filename = `${filename}.${ext}`; | ||
contentType, | ||
contentEncoding: metadata.contentEncoding, | ||
contentLanguage: metadata.contentLanguage, | ||
cacheControl: metadata.cacheControl, | ||
localFile: file, | ||
@@ -147,2 +161,32 @@ source: createReadStream(file), | ||
/** | ||
* Convert blob to asset structure | ||
*/ | ||
static async blobToAsset(blob: Blob, metadata: Partial<StreamMeta> = {}): Promise<Asset> { | ||
const hash = metadata.hash ??= await this.computeHash(blob); | ||
const size = metadata.size ?? blob.size; | ||
const contentType = metadata.contentType ?? blob.type; | ||
let filename = metadata.filename; | ||
if (!filename) { | ||
filename = `unknown.${Date.now()}`; | ||
const ext = this.getExtension(contentType); | ||
if (ext) { | ||
filename = `${filename}.${ext}`; | ||
} | ||
} | ||
return { | ||
size, | ||
filename, | ||
contentType, | ||
contentEncoding: metadata.contentEncoding, | ||
contentLanguage: metadata.contentLanguage, | ||
cacheControl: metadata.cacheControl, | ||
source: Readable.fromWeb(blob.stream()), | ||
hash | ||
}; | ||
} | ||
/** | ||
* Fetch bytes from a url | ||
@@ -158,3 +202,3 @@ */ | ||
if (!str.ok) { | ||
throw new AppError('Invalid url for hashing', 'data'); | ||
throw new Error('Invalid url for hashing'); | ||
} | ||
@@ -190,6 +234,4 @@ | ||
static async hashUrl(url: string, byteLimit = -1): Promise<string> { | ||
const hasher = crypto.createHash('sha256').setEncoding('hex'); | ||
const finalData = await this.fetchBytes(url, byteLimit); | ||
return hasher.update(finalData).end().read().toString(); | ||
return this.computeHash(await this.fetchBytes(url, byteLimit)); | ||
} | ||
} |
@@ -43,3 +43,3 @@ import assert from 'node:assert'; | ||
const outHashed = await service.upsert(file, false, new HashNamingStrategy()); | ||
const hash = await AssetUtil.hashFile(pth); | ||
const hash = await AssetUtil.computeHash(pth); | ||
assert(outHashed.replace(/\//g, '').replace(/[.][^.]+$/, '') === hash); | ||
@@ -55,3 +55,3 @@ } | ||
const saved = await service.get(loc); | ||
const { meta: saved } = await service.get(loc); | ||
@@ -58,0 +58,0 @@ assert(file.contentType === saved.contentType); |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
32020
18
487
1
141
+ Added@travetto/config@5.1.0(transitive)
+ Added@travetto/di@5.1.0(transitive)
+ Added@travetto/manifest@5.1.0(transitive)
+ Added@travetto/model@5.1.0(transitive)
+ Added@travetto/registry@5.1.0(transitive)
+ Added@travetto/runtime@5.1.0(transitive)
+ Added@travetto/schema@5.1.0(transitive)
+ Added@travetto/terminal@5.1.0(transitive)
+ Added@travetto/test@5.1.0(transitive)
+ Added@travetto/worker@5.1.0(transitive)
+ Added@types/node@22.13.4(transitive)
+ Addedundici-types@6.20.0(transitive)
+ Addedyaml@2.7.0(transitive)
- Removed@travetto/base@4.1.2(transitive)
- Removed@travetto/config@4.1.2(transitive)
- Removed@travetto/di@4.1.1(transitive)
- Removed@travetto/manifest@4.1.0(transitive)
- Removed@travetto/model@4.1.4(transitive)
- Removed@travetto/registry@4.1.2(transitive)
- Removed@travetto/schema@4.1.1(transitive)
- Removed@travetto/terminal@4.1.1(transitive)
- Removed@travetto/test@4.1.1(transitive)
- Removed@travetto/worker@4.1.1(transitive)
- Removed@travetto/yaml@4.1.1(transitive)
- Removed@types/node@20.17.19(transitive)
- Removedundici-types@6.19.8(transitive)
Updated@travetto/di@^5.0.0-rc.0
Updated@travetto/model@^5.0.0-rc.0