Compare commits

..

14 Commits

14 changed files with 4491 additions and 2263 deletions

86
changelog.md Normal file
View File

@ -0,0 +1,86 @@
# Changelog
## 2024-11-18 - 3.1.0 - feat(file)
Added functionality to retrieve magic bytes from files and detect file types using magic bytes.
- Introduced method `getMagicBytes` in `File` and `Bucket` classes to retrieve a specific number of bytes from a file.
- Enhanced file type detection by utilizing magic bytes in `MetaData` class.
- Updated dependencies for better performance and compatibility.
## 2024-11-18 - 3.0.24 - fix(metadata)
Fix metadata handling to address type assertion and data retrieval.
- Fixed type assertion issues in `MetaData` class properties with type non-null assertions.
- Corrected the handling of JSON data retrieval in `MetaData.storeCustomMetaData` function.
## 2024-10-16 - 3.0.23 - fix(dependencies)
Update package dependencies for improved functionality and security.
- Updated @aws-sdk/client-s3 to version ^3.670.0 for enhanced S3 client capabilities.
- Updated @push.rocks/smartstream to version ^3.2.4.
- Updated the dev dependency @push.rocks/tapbundle to version ^5.3.0.
## 2024-07-28 - 3.0.22 - fix(dependencies)
Update dependencies and improve bucket retrieval logging
- Updated @aws-sdk/client-s3 to ^3.620.0
- Updated @git.zone/tsbuild to ^2.1.84
- Updated @git.zone/tsrun to ^1.2.49
- Updated @push.rocks/smartpromise to ^4.0.4
- Updated @tsclass/tsclass to ^4.1.2
- Added a log for when a bucket is not found by name in getBucketByName method
## 2024-07-04 - 3.0.21 - fix(test)
Update endpoint configuration in tests to use environment variable
- Modified `qenv.yml` to include `S3_ENDPOINT` as a required environment variable.
- Updated test files to fetch `S3_ENDPOINT` from environment instead of hardcoding.
## 2024-06-19 - 3.0.20 - Fix and Stability Updates
Improved overall stability and consistency.
## 2024-06-18 - 3.0.18 - Delete Functions Consistency
Ensured more consistency between delete methods and trash behavior.
## 2024-06-17 - 3.0.17 to 3.0.16 - Fix and Update
Routine updates and fixes performed.
## 2024-06-11 - 3.0.15 to 3.0.14 - Fix and Update
Routine updates and fixes performed.
## 2024-06-10 - 3.0.13 - Trash Feature Completion
Finished work on trash feature.
## 2024-06-09 - 3.0.12 - Fix and Update
Routine updates and fixes performed.
## 2024-06-08 - 3.0.11 to 3.0.10 - Fix and Update
Routine updates and fixes performed.
## 2024-06-03 - 3.0.10 - Fix and Update
Routine updates and fixes performed.
## 2024-05-29 - 3.0.9 - Update Description
Updated project description.
## 2024-05-27 - 3.0.8 to 3.0.6 - Pathing and Core Updates
Routine updates and fixes performed.
- S3 paths' pathing differences now correctly handled with a reducePath method.
## 2024-05-21 - 3.0.5 to 3.0.4 - Fix and Update
Routine updates and fixes performed.
## 2024-05-17 - 3.0.3 to 3.0.2 - Fix and Update
Routine updates and fixes performed.
## 2024-05-17 - 3.0.0 - Major Release
Introduced breaking changes in core and significant improvements.
## 2024-05-05 - 2.0.5 - Breaking Changes
Introduced breaking changes in core functionality.
## 2024-04-14 - 2.0.4 - TSConfig Update
Updated TypeScript configuration.
## 2024-01-01 - 2.0.2 - Organization Scheme Update
Switched to the new organizational scheme.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@push.rocks/smartbucket", "name": "@push.rocks/smartbucket",
"version": "3.0.18", "version": "3.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@push.rocks/smartbucket", "name": "@push.rocks/smartbucket",
"version": "3.0.18", "version": "3.1.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^5.0.18",

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartbucket", "name": "@push.rocks/smartbucket",
"version": "3.0.18", "version": "3.1.0",
"description": "A TypeScript library offering simple and cloud-agnostic object storage with advanced features like bucket creation, file and directory management, and data streaming.", "description": "A TypeScript library offering simple and cloud-agnostic object storage with advanced features like bucket creation, file and directory management, and data streaming.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
@ -12,22 +12,22 @@
"build": "(tsbuild --web --allowimplicitany)" "build": "(tsbuild --web --allowimplicitany)"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.80", "@git.zone/tsbuild": "^2.1.84",
"@git.zone/tsrun": "^1.2.46", "@git.zone/tsrun": "^1.2.49",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.90",
"@push.rocks/qenv": "^6.0.5", "@push.rocks/qenv": "^6.0.5",
"@push.rocks/tapbundle": "^5.0.23" "@push.rocks/tapbundle": "^5.3.0"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.598.0", "@aws-sdk/client-s3": "^3.693.0",
"@push.rocks/smartmime": "^2.0.2", "@push.rocks/smartmime": "^2.0.4",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrx": "^3.0.7", "@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartstream": "^3.0.44", "@push.rocks/smartstream": "^3.2.4",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^4.0.59" "@tsclass/tsclass": "^4.1.2"
}, },
"private": false, "private": false,
"files": [ "files": [

6480
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
required: required:
- S3_KEY - S3_KEY
- S3_SECRET - S3_SECRET
- S3_ENDPOINT

28
test/test.trash.ts Normal file
View File

@ -0,0 +1,28 @@
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as smartbucket from '../ts/index.js';
const testQenv = new Qenv('./', './.nogit/');
let testSmartbucket: smartbucket.SmartBucket;
let myBucket: smartbucket.Bucket;
let baseDirectory: smartbucket.Directory;
tap.test('should create a valid smartbucket', async () => {
testSmartbucket = new smartbucket.SmartBucket({
accessKey: await testQenv.getEnvVarOnDemand('S3_KEY'),
accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRET'),
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
});
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
myBucket = await testSmartbucket.getBucketByName('testzone');
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
expect(myBucket.name).toEqual('testzone');
});
tap.test('', async () => {
})
export default tap.start();

View File

@ -13,24 +13,22 @@ tap.test('should create a valid smartbucket', async () => {
testSmartbucket = new smartbucket.SmartBucket({ testSmartbucket = new smartbucket.SmartBucket({
accessKey: await testQenv.getEnvVarOnDemand('S3_KEY'), accessKey: await testQenv.getEnvVarOnDemand('S3_KEY'),
accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRET'), accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRET'),
endpoint: 's3.eu-central-1.wasabisys.com', endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
}); });
}); expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
tap.skip.test('should create testbucket', async () => {
// await testSmartbucket.createBucket('testzone');
});
tap.skip.test('should remove testbucket', async () => {
// await testSmartbucket.removeBucket('testzone');
});
tap.test('should get a bucket', async () => {
myBucket = await testSmartbucket.getBucketByName('testzone'); myBucket = await testSmartbucket.getBucketByName('testzone');
expect(myBucket).toBeInstanceOf(smartbucket.Bucket); expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
expect(myBucket.name).toEqual('testzone'); expect(myBucket.name).toEqual('testzone');
}); });
tap.skip.test('should create testbucket', async () => {
// await testSmartbucket.createBucket('testzone2');
});
tap.skip.test('should remove testbucket', async () => {
// await testSmartbucket.removeBucket('testzone2');
});
// Fast operations // Fast operations
tap.test('should store data in bucket fast', async () => { tap.test('should store data in bucket fast', async () => {
await myBucket.fastPut({ await myBucket.fastPut({

View File

@ -1,8 +1,8 @@
/** /**
* autocreated commitinfo by @pushrocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartbucket', name: '@push.rocks/smartbucket',
version: '3.0.18', version: '3.1.0',
description: 'A TypeScript library offering simple and cloud-agnostic object storage with advanced features like bucket creation, file and directory management, and data streaming.' description: 'A TypeScript library offering simple and cloud-agnostic object storage with advanced features like bucket creation, file and directory management, and data streaming.'
} }

View File

@ -24,6 +24,7 @@ export class Bucket {
console.log(`Taking this as base for new Bucket instance`); console.log(`Taking this as base for new Bucket instance`);
return new this(smartbucketRef, bucketNameArg); return new this(smartbucketRef, bucketNameArg);
} else { } else {
console.log(`did not find bucket by name: ${bucketNameArg}`);
return null; return null;
} }
} }
@ -231,6 +232,7 @@ export class Bucket {
if (typeArg === 'webstream') { if (typeArg === 'webstream') {
return (await duplexStream.getWebStreams()).readable; return (await duplexStream.getWebStreams()).readable;
} }
throw new Error('unknown typeArg');
} }
/** /**
@ -436,4 +438,28 @@ export class Bucket {
const response = await this.smartbucketRef.s3Client.send(command); const response = await this.smartbucketRef.s3Client.send(command);
return response.Contents.length > 0; return response.Contents.length > 0;
} }
public async getMagicBytes(optionsArg: { path: string; length: number }): Promise<Buffer> {
try {
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
Range: `bytes=0-${optionsArg.length - 1}`,
});
const response = await this.smartbucketRef.s3Client.send(command);
const chunks = [];
const stream = response.Body as any; // SdkStreamMixin includes readable stream
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
} catch (error) {
console.error(
`Error retrieving magic bytes from object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
throw error;
}
}
} }

View File

@ -66,13 +66,13 @@ export class Directory {
* gets a file by name * gets a file by name
*/ */
public async getFile(optionsArg: { public async getFile(optionsArg: {
name: string; path: string;
createWithContents?: string | Buffer; createWithContents?: string | Buffer;
getFromTrash?: boolean; getFromTrash?: boolean;
}): Promise<File> { }): Promise<File> {
const pathDescriptor = { const pathDescriptor = {
directory: this, directory: this,
path: optionsArg.name, path: optionsArg.path,
}; };
const exists = await this.bucketRef.fastExists({ const exists = await this.bucketRef.fastExists({
path: await helpers.reducePathDescriptorToPath(pathDescriptor), path: await helpers.reducePathDescriptorToPath(pathDescriptor),
@ -88,13 +88,13 @@ export class Directory {
if (!exists && optionsArg.createWithContents) { if (!exists && optionsArg.createWithContents) {
await File.create({ await File.create({
directory: this, directory: this,
name: optionsArg.name, name: optionsArg.path,
contents: optionsArg.createWithContents, contents: optionsArg.createWithContents,
}); });
} }
return new File({ return new File({
directoryRefArg: this, directoryRefArg: this,
fileName: optionsArg.name, fileName: optionsArg.path,
}); });
} }
@ -283,19 +283,30 @@ export class Directory {
/** /**
* removes a file within the directory * removes a file within the directory
* uses file class to make sure effects for metadata etc. are handled correctly
* @param optionsArg * @param optionsArg
*/ */
public async fastRemove(optionsArg: { path: string }) { public async fastRemove(optionsArg: {
const path = plugins.path.join(this.getBasePath(), optionsArg.path); path: string
await this.bucketRef.fastRemove({ /**
path, * wether the file should be placed into trash. Default is false.
*/
mode?: 'permanent' | 'trash';
}) {
const file = await this.getFile({
path: optionsArg.path,
});
await file.delete({
mode: optionsArg.mode ? optionsArg.mode : 'permanent',
}); });
} }
/** /**
* deletes the directory with all its contents * deletes the directory with all its contents
*/ */
public async delete() { public async delete(optionsArg: {
mode?: 'permanent' | 'trash';
}) {
const deleteDirectory = async (directoryArg: Directory) => { const deleteDirectory = async (directoryArg: Directory) => {
const childDirectories = await directoryArg.listDirectories(); const childDirectories = await directoryArg.listDirectories();
if (childDirectories.length === 0) { if (childDirectories.length === 0) {
@ -307,9 +318,9 @@ export class Directory {
} }
const files = await directoryArg.listFiles(); const files = await directoryArg.listFiles();
for (const file of files) { for (const file of files) {
await directoryArg.fastRemove({ await file.delete({
path: file.name, mode: optionsArg.mode ? optionsArg.mode : 'permanent',
}); })
} }
}; };
await deleteDirectory(this); await deleteDirectory(this);

View File

@ -50,6 +50,10 @@ export class File {
public parentDirectoryRef: Directory; public parentDirectoryRef: Directory;
public name: string; public name: string;
/**
* get the full path to the file
* @returns the full path to the file
*/
public getBasePath(): string { public getBasePath(): string {
return plugins.path.join(this.parentDirectoryRef.getBasePath(), this.name); return plugins.path.join(this.parentDirectoryRef.getBasePath(), this.name);
} }
@ -188,7 +192,7 @@ export class File {
if (isDirectory) { if (isDirectory) {
moveToPath = await helpers.reducePathDescriptorToPath({ moveToPath = await helpers.reducePathDescriptorToPath({
...pathDescriptorArg, ...pathDescriptorArg,
path: plugins.path.join(pathDescriptorArg.path, this.name), path: plugins.path.join(pathDescriptorArg.path!, this.name),
}); });
} }
// lets move the file // lets move the file
@ -230,4 +234,11 @@ export class File {
contents: JSON.stringify(dataArg), contents: JSON.stringify(dataArg),
}); });
} }
public async getMagicBytes(optionsArg: { length: number }): Promise<Buffer> {
return this.parentDirectoryRef.bucketRef.getMagicBytes({
path: this.getBasePath(),
length: optionsArg.length
})
}
} }

View File

@ -10,7 +10,7 @@ export class MetaData {
// lets find the existing metadata file // lets find the existing metadata file
metaData.metadataFile = await metaData.fileRef.parentDirectoryRef.getFile({ metaData.metadataFile = await metaData.fileRef.parentDirectoryRef.getFile({
name: metaData.fileRef.name + '.metadata', path: metaData.fileRef.name + '.metadata',
createWithContents: '{}', createWithContents: '{}',
}); });
@ -21,20 +21,34 @@ export class MetaData {
/** /**
* the file that contains the metadata * the file that contains the metadata
*/ */
metadataFile: File; metadataFile!: File;
/** /**
* the file that the metadata is for * the file that the metadata is for
*/ */
fileRef: File; fileRef!: File;
public async getFileType(optionsArg?: { public async getFileType(optionsArg?: {
useFileExtension?: boolean; useFileExtension?: boolean;
useMagicBytes?: boolean; useMagicBytes?: boolean;
}): Promise<string> { }): Promise<plugins.smartmime.IFileTypeResult | undefined> {
if ((optionsArg && optionsArg.useFileExtension) || optionsArg.useFileExtension === undefined) { if ((optionsArg && optionsArg.useFileExtension) || !optionsArg) {
return plugins.path.extname(this.fileRef.name); const fileType = await plugins.smartmime.detectMimeType({
path: this.fileRef.name,
});
return fileType;
} }
if (optionsArg && optionsArg.useMagicBytes) {
const fileType = await plugins.smartmime.detectMimeType({
buffer: await this.fileRef.getMagicBytes({
length: 100,
})
});
return fileType;
}
throw new Error('optionsArg.useFileExtension and optionsArg.useMagicBytes cannot both be false');
} }
/** /**
@ -44,13 +58,13 @@ export class MetaData {
const stat = await this.fileRef.parentDirectoryRef.bucketRef.fastStat({ const stat = await this.fileRef.parentDirectoryRef.bucketRef.fastStat({
path: this.fileRef.getBasePath(), path: this.fileRef.getBasePath(),
}); });
return stat.ContentLength; return stat.ContentLength!;
} }
private prefixCustomMetaData = 'custom_'; private prefixCustomMetaData = 'custom_';
public async storeCustomMetaData<T = any>(optionsArg: { key: string; value: T }) { public async storeCustomMetaData<T = any>(optionsArg: { key: string; value: T }) {
const data = await this.metadataFile.getContentsAsString(); const data = await this.metadataFile.getJsonData();
data[this.prefixCustomMetaData + optionsArg.key] = optionsArg.value; data[this.prefixCustomMetaData + optionsArg.key] = optionsArg.value;
await this.metadataFile.writeJsonData(data); await this.metadataFile.writeJsonData(data);
} }

View File

@ -21,7 +21,7 @@ export class Trash {
const trashDir = await this.getTrashDir(); const trashDir = await this.getTrashDir();
const originalPath = await helpers.reducePathDescriptorToPath(pathDescriptor); const originalPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const trashKey = await this.getTrashKeyByOriginalBasePath(originalPath); const trashKey = await this.getTrashKeyByOriginalBasePath(originalPath);
return trashDir.getFile({ name: trashKey }); return trashDir.getFile({ path: trashKey });
} }
public async getTrashKeyByOriginalBasePath (originalPath: string): Promise<string> { public async getTrashKeyByOriginalBasePath (originalPath: string): Promise<string> {

View File

@ -6,7 +6,8 @@
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"strict": true
}, },
"exclude": [ "exclude": [
"dist_*/**/*.d.ts" "dist_*/**/*.d.ts"