Compare commits

..

42 Commits

Author SHA1 Message Date
8e9041fbbf 3.2.2 2024-11-24 03:05:10 +01:00
16a82ac50a fix(core): Refactor Bucket class for improved error handling 2024-11-24 03:05:10 +01:00
0b396f19cf 3.2.1 2024-11-24 02:28:48 +01:00
6ab77ece6e fix(metadata): Fix metadata handling for deleted files 2024-11-24 02:28:48 +01:00
b7a1f2087c Merge pull request 'fix: use overwrite to make metadata files work' (#2) from fix/smartbucket-trash into master
Reviewed-on: #2
2024-11-24 01:27:58 +00:00
b0d41fa9a0 3.2.0 2024-11-24 02:25:08 +01:00
34082c38a7 feat(bucket): Enhanced SmartBucket with trash management and metadata handling 2024-11-24 02:25:08 +01:00
8d160cefb0 fix: use overwrite to make metadata files work
During a delete the metadata file is updated. As the overwrite property was not set, the metadata
couldn't be updated and caused issues.
2024-11-18 21:08:39 +00:00
cec9c07b7c 3.1.0 2024-11-18 15:07:47 +01:00
383a5204f4 feat(file): Added functionality to retrieve magic bytes from files and detect file types using magic bytes. 2024-11-18 15:07:46 +01:00
c7f0c97341 3.0.24 2024-11-18 11:24:11 +01:00
e7f60465ff fix(metadata): Fix metadata handling to address type assertion and data retrieval. 2024-11-18 11:24:11 +01:00
7db4d24817 3.0.23 2024-10-16 10:27:27 +02:00
dc599585b8 fix(dependencies): Update package dependencies for improved functionality and security. 2024-10-16 10:27:27 +02:00
a22e32cd32 3.0.22 2024-07-28 12:46:39 +02:00
4647181807 fix(dependencies): Update dependencies and improve bucket retrieval logging 2024-07-28 12:46:39 +02:00
99c3935d0c 3.0.21 2024-07-04 18:39:28 +02:00
05523dc7a1 fix(test): Update endpoint configuration in tests to use environment variable 2024-07-04 18:39:27 +02:00
dc99cfa229 3.0.20 2024-06-19 18:28:53 +02:00
23f8dc55d0 fix(core): update 2024-06-19 18:28:52 +02:00
ffaf0fc97a 3.0.19 2024-06-18 18:44:59 +02:00
2a0425ff54 fix(delete functions): ensure more consistency between methods and trash behaviour 2024-06-18 18:44:58 +02:00
9adcdee0a0 3.0.18 2024-06-17 20:00:58 +02:00
786f8d4365 fix(core): update 2024-06-17 20:00:57 +02:00
67244ba5cf 3.0.17 2024-06-17 19:57:56 +02:00
a9bb31c2a2 fix(core): update 2024-06-17 19:57:56 +02:00
bd8b05920f 3.0.16 2024-06-17 16:01:36 +02:00
535d9f8520 fix(core): update 2024-06-17 16:01:35 +02:00
8401fe1c0c 3.0.15 2024-06-11 17:21:22 +02:00
08c3f674bf fix(core): update 2024-06-11 17:21:22 +02:00
df0a439def 3.0.14 2024-06-11 17:20:49 +02:00
7245b49c31 fix(core): update 2024-06-11 17:20:48 +02:00
4b70edb947 finish trash 2024-06-10 16:47:20 +02:00
9629a04da6 3.0.13 2024-06-09 16:32:33 +02:00
963463d40d fix(core): update 2024-06-09 16:32:32 +02:00
ce58b99fc7 3.0.12 2024-06-09 16:02:34 +02:00
591c99736d fix(core): update 2024-06-09 16:02:33 +02:00
559e3da47b 3.0.11 2024-06-08 19:13:25 +02:00
a7ac870e05 fix(core): update 2024-06-08 19:13:24 +02:00
d48c5e229a 3.0.10 2024-06-03 21:35:08 +02:00
b9c384dd08 fix(core): update 2024-06-03 21:35:08 +02:00
91c04b2364 update description 2024-05-29 14:11:54 +02:00
18 changed files with 5261 additions and 1892 deletions

107
changelog.md Normal file
View File

@ -0,0 +1,107 @@
# Changelog
## 2024-11-24 - 3.2.2 - fix(core)
Refactor Bucket class for improved error handling
- Ensured safe access using non-null assertions when finding a bucket.
- Enhanced fastPut method by adding fastPutStrict for safer operations.
- Added explicit error handling and type checking in fastExists method.
## 2024-11-24 - 3.2.1 - fix(metadata)
Fix metadata handling for deleted files
- Ensured metadata is correctly stored and managed when files are deleted into the trash.
## 2024-11-24 - 3.2.0 - feat(bucket)
Enhanced SmartBucket with trash management and metadata handling
- Added functionality to move files to a trash directory.
- Introduced methods to handle file metadata more robustly.
- Implemented a method to clean all contents from a bucket.
- Enhanced directory retrieval to handle non-existent directories with options.
- Improved handling of file paths and metadata within the storage system.
## 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",
"version": "3.0.9",
"version": "3.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@push.rocks/smartbucket",
"version": "3.0.9",
"version": "3.2.2",
"license": "UNLICENSED",
"dependencies": {
"@push.rocks/smartpath": "^5.0.18",

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartbucket",
"version": "3.0.9",
"version": "3.2.2",
"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",
"typings": "dist_ts/index.d.ts",
@ -12,20 +12,22 @@
"build": "(tsbuild --web --allowimplicitany)"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.80",
"@git.zone/tsrun": "^1.2.46",
"@git.zone/tsbuild": "^2.1.84",
"@git.zone/tsrun": "^1.2.49",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/qenv": "^6.0.5",
"@push.rocks/tapbundle": "^5.0.23"
"@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^5.5.3"
},
"dependencies": {
"@push.rocks/smartmime": "^2.0.0",
"@aws-sdk/client-s3": "^3.699.0",
"@push.rocks/smartmime": "^2.0.4",
"@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/smartstream": "^3.0.38",
"@tsclass/tsclass": "^4.0.54",
"minio": "^8.0.0"
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^4.1.2"
},
"private": false,
"files": [
@ -62,5 +64,10 @@
"access key",
"secret key",
"cloud agnostic"
]
],
"homepage": "https://code.foss.global/push.rocks/smartbucket",
"repository": {
"type": "git",
"url": "https://code.foss.global/push.rocks/smartbucket.git"
}
}

5839
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

0
test/helpers/prepare.ts Normal file
View File

7
test/test.metadata.ts Normal file
View File

@ -0,0 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle';
tap.test('test metadata functionality', async () => {
})
tap.start();

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

@ -0,0 +1,63 @@
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
import { jestExpect } from '@push.rocks/tapbundle/node';
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.getEnvVarOnDemandStrict('S3_ACCESSKEY'),
accessSecret: await testQenv.getEnvVarOnDemandStrict('S3_ACCESSSECRET'),
endpoint: await testQenv.getEnvVarOnDemandStrict('S3_ENDPOINT'),
});
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
myBucket = await testSmartbucket.getBucketByNameStrict(await testQenv.getEnvVarOnDemandStrict('S3_BUCKET'),);
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
expect(myBucket.name).toEqual('test-pushrocks-smartbucket');
});
tap.test('should clean all contents', async () => {
await myBucket.cleanAllContents();
expect(await myBucket.fastExists({ path: 'hithere/socool.txt' })).toBeFalse();
expect(await myBucket.fastExists({ path: 'trashtest/trashme.txt' })).toBeFalse();
});
tap.test('should delete a file into the normally', async () => {
const path = 'trashtest/trashme.txt';
const file = await myBucket.fastPut({
path,
contents: 'I\'m in the trash test content!',
});
const fileMetadata = await (await file.getMetaData()).metadataFile.getContents();
console.log(fileMetadata.toString());
expect(await file.getMetaData().then((meta) => meta.metadataFile.getJsonData())).toEqual({});
await file.delete({ mode: 'permanent' });
expect((await (await myBucket.getBaseDirectory()).listFiles()).length).toEqual(0);
expect((await (await myBucket.getBaseDirectory()).listDirectories()).length).toEqual(0);
});
tap.test('should put a file into the trash', async () => {
const path = 'trashtest/trashme.txt';
const file = await myBucket.fastPut({
path,
contents: 'I\'m in the trash test content!',
});
const fileMetadata = await (await file.getMetaData()).metadataFile.getContents();
console.log(fileMetadata.toString());
expect(await file.getMetaData().then((meta) => meta.metadataFile.getJsonData())).toEqual({});
await file.delete({ mode: 'trash' });
jestExpect(await file.getMetaData().then((meta) => meta.metadataFile.getJsonData())).toEqual({
custom_recycle: {
deletedAt: jestExpect.any(Number),
originalPath: "trashtest/trashme.txt",
},
});
});
export default tap.start();

View File

@ -11,24 +11,28 @@ 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: 's3.eu-central-1.wasabisys.com',
accessKey: await testQenv.getEnvVarOnDemandStrict('S3_ACCESSKEY'),
accessSecret: await testQenv.getEnvVarOnDemandStrict('S3_ACCESSSECRET'),
endpoint: await testQenv.getEnvVarOnDemandStrict('S3_ENDPOINT'),
});
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
myBucket = await testSmartbucket.getBucketByNameStrict(await testQenv.getEnvVarOnDemandStrict('S3_BUCKET'),);
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
expect(myBucket.name).toEqual('test-pushrocks-smartbucket');
});
tap.test('should clean all contents', async () => {
await myBucket.cleanAllContents();
expect(await myBucket.fastExists({ path: 'hithere/socool.txt' })).toBeFalse();
expect(await myBucket.fastExists({ path: 'trashtest/trashme.txt' })).toBeFalse();
});
tap.skip.test('should create testbucket', async () => {
// await testSmartbucket.createBucket('testzone');
// await testSmartbucket.createBucket('testzone2');
});
tap.skip.test('should remove testbucket', async () => {
// await testSmartbucket.removeBucket('testzone');
});
tap.test('should get a bucket', async () => {
myBucket = await testSmartbucket.getBucketByName('testzone');
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
expect(myBucket.name).toEqual('testzone');
// await testSmartbucket.removeBucket('testzone2');
});
// Fast operations
@ -43,9 +47,12 @@ tap.test('should get data in bucket', async () => {
const fileString = await myBucket.fastGet({
path: 'hithere/socool.txt',
});
const fileStringStream = await myBucket.fastGetStream({
path: 'hithere/socool.txt',
});
const fileStringStream = await myBucket.fastGetStream(
{
path: 'hithere/socool.txt',
},
'nodestream'
);
console.log(fileString);
});
@ -99,8 +106,9 @@ tap.test('should get base directory', async () => {
tap.test('should correctly build paths for sub directories', async () => {
const dir4 = await baseDirectory.getSubDirectoryByName('dir3/dir4');
expect(dir4).toBeInstanceOf(smartbucket.Directory);
const dir4BasePath = dir4.getBasePath();
const dir4BasePath = dir4?.getBasePath();
console.log(dir4BasePath);
expect(dir4BasePath).toEqual('dir3/dir4/');
});
tap.test('clean up directory style tests', async () => {

View File

@ -1,8 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/smartbucket',
version: '3.0.9',
version: '3.2.2',
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

@ -1,33 +1,43 @@
// classes.bucket.ts
import * as plugins from './plugins.js';
import * as helpers from './helpers.js';
import * as interfaces from './interfaces.js';
import { SmartBucket } from './classes.smartbucket.js';
import { Directory } from './classes.directory.js';
import { File } from './classes.file.js';
import { Trash } from './classes.trash.js';
/**
* The bucket class exposes the basic functionality of a bucket.
* The functions of the bucket alone are enough to
* operate in S3 basic fashion on blobs of data.
*/
export class Bucket {
public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) {
const buckets = await smartbucketRef.minioClient.listBuckets();
const foundBucket = buckets.find((bucket) => {
return bucket.name === bucketNameArg;
});
const command = new plugins.s3.ListBucketsCommand({});
const buckets = await smartbucketRef.s3Client.send(command);
const foundBucket = buckets.Buckets!.find((bucket) => bucket.Name === bucketNameArg);
if (foundBucket) {
console.log(`bucket with name ${bucketNameArg} exists.`);
console.log(`Taking this as base for new Bucket instance`);
return new this(smartbucketRef, bucketNameArg);
} else {
console.log(`did not find bucket by name: ${bucketNameArg}`);
return null;
}
}
public static async createBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
await smartbucketRef.minioClient.makeBucket(bucketName, 'ams3').catch((e) => console.log(e));
const command = new plugins.s3.CreateBucketCommand({ Bucket: bucketName });
await smartbucketRef.s3Client.send(command).catch((e) => console.log(e));
return new Bucket(smartbucketRef, bucketName);
}
public static async removeBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
await smartbucketRef.minioClient.removeBucket(bucketName).catch((e) => console.log(e));
const command = new plugins.s3.DeleteBucketCommand({ Bucket: bucketName });
await smartbucketRef.s3Client.send(command).catch((e) => console.log(e));
}
public smartbucketRef: SmartBucket;
@ -42,16 +52,28 @@ export class Bucket {
* gets the base directory of the bucket
*/
public async getBaseDirectory(): Promise<Directory> {
return new Directory(this, null, '');
return new Directory(this, null!, '');
}
public async getDirectoryFromPath(pathDescriptorArg: interfaces.IPathDecriptor): Promise<Directory> {
/**
* gets the trash directory
*/
public async getTrash(): Promise<Trash> {
const trash = new Trash(this);
return trash;
}
public async getDirectoryFromPath(
pathDescriptorArg: interfaces.IPathDecriptor
): Promise<Directory> {
if (!pathDescriptorArg.path && !pathDescriptorArg.directory) {
return this.getBaseDirectory();
}
let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
const baseDirectory = await this.getBaseDirectory();
return await baseDirectory.getSubDirectoryByName(checkPath);
return await baseDirectory.getSubDirectoryByNameStrict(checkPath, {
getEmptyDirectory: true,
});
}
// ===============
@ -61,34 +83,35 @@ export class Bucket {
/**
* store file
*/
public async fastPut(optionsArg: {
path: string;
contents: string | Buffer;
overwrite?: boolean;
}): Promise<File> {
public async fastPut(
optionsArg: interfaces.IPathDecriptor & {
contents: string | Buffer;
overwrite?: boolean;
}
): Promise<File | null> {
try {
const reducedPath = await helpers.reducePathDescriptorToPath({
path: optionsArg.path,
})
// Check if the object already exists
const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
const exists = await this.fastExists({ path: reducedPath });
if (exists && !optionsArg.overwrite) {
console.error(`Object already exists at path '${reducedPath}' in bucket '${this.name}'.`);
return;
const errorText = `Object already exists at path '${reducedPath}' in bucket '${this.name}'.`;
console.error(errorText);
return null;
} else if (exists && optionsArg.overwrite) {
console.log(`Overwriting existing object at path '${reducedPath}' in bucket '${this.name}'.`);
console.log(
`Overwriting existing object at path '${reducedPath}' in bucket '${this.name}'.`
);
} else {
console.log(`Creating new object at path '${reducedPath}' in bucket '${this.name}'.`);
}
// Proceed with putting the object
const streamIntake = new plugins.smartstream.StreamIntake();
const putPromise = this.smartbucketRef.minioClient.putObject(this.name, reducedPath, streamIntake);
streamIntake.pushData(optionsArg.contents);
streamIntake.signalEnd();
await putPromise;
const command = new plugins.s3.PutObjectCommand({
Bucket: this.name,
Key: reducedPath,
Body: optionsArg.contents,
});
await this.smartbucketRef.s3Client.send(command);
console.log(`Object '${reducedPath}' has been successfully stored in bucket '${this.name}'.`);
const parsedPath = plugins.path.parse(reducedPath);
return new File({
@ -98,19 +121,29 @@ export class Bucket {
fileName: parsedPath.base,
});
} catch (error) {
console.error(`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`, error);
console.error(
`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
throw error;
}
}
public async fastPutStrict(...args: Parameters<Bucket['fastPut']>) {
const file = await this.fastPut(...args);
if (!file) {
throw new Error(`File not stored at path '${args[0].path}'`);
}
return file;
}
/**
* get file
*/
public async fastGet(optionsArg: Parameters<typeof this.fastGetStream>[0]): Promise<Buffer> {
public async fastGet(optionsArg: { path: string }): Promise<Buffer> {
const done = plugins.smartpromise.defer();
let completeFile: Buffer;
const replaySubject = await this.fastGetStream(optionsArg);
const replaySubject = await this.fastGetReplaySubject(optionsArg);
const subscription = replaySubject.subscribe({
next: (chunk) => {
if (completeFile) {
@ -128,82 +161,135 @@ export class Bucket {
},
});
await done.promise;
return completeFile;
return completeFile!;
}
public async fastGetStream(optionsArg: {
/**
* good when time to first byte is important
* and multiple subscribers are expected
* @param optionsArg
* @returns
*/
public async fastGetReplaySubject(optionsArg: {
path: string;
}): Promise<plugins.smartrx.rxjs.ReplaySubject<Buffer>> {
const fileStream = await this.smartbucketRef.minioClient
.getObject(this.name, optionsArg.path)
.catch((e) => console.log(e));
const replaySubject = new plugins.smartrx.rxjs.ReplaySubject<Buffer>();
const duplexStream = new plugins.smartstream.SmartDuplex<Buffer, void>({
writeFunction: async (chunk) => {
replaySubject.next(chunk);
return;
},
finalFunction: async (cb) => {
replaySubject.complete();
return;
}
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
const response = await this.smartbucketRef.s3Client.send(command);
const replaySubject = new plugins.smartrx.rxjs.ReplaySubject<Buffer>();
if (!fileStream) {
return null;
// Convert the stream to a format that supports piping
const stream = response.Body as any; // SdkStreamMixin includes readable stream
if (typeof stream.pipe === 'function') {
const duplexStream = new plugins.smartstream.SmartDuplex<Buffer, void>({
writeFunction: async (chunk) => {
replaySubject.next(chunk);
return;
},
finalFunction: async (cb) => {
replaySubject.complete();
return;
},
});
stream.pipe(duplexStream);
}
const smartstream = new plugins.smartstream.StreamWrapper([
fileStream,
duplexStream,
]);
smartstream.run();
return replaySubject;
}
public fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'webstream'
): Promise<ReadableStream>;
public async fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'nodestream'
): Promise<plugins.stream.Readable>;
public async fastGetStream(
optionsArg: { path: string },
typeArg: 'webstream' | 'nodestream' = 'nodestream'
): Promise<ReadableStream | plugins.stream.Readable> {
const command = new plugins.s3.GetObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
const response = await this.smartbucketRef.s3Client.send(command);
const stream = response.Body as any; // SdkStreamMixin includes readable stream
const duplexStream = new plugins.smartstream.SmartDuplex<Buffer, Buffer>({
writeFunction: async (chunk) => {
return chunk;
},
finalFunction: async (cb) => {
return null!;
},
});
if (typeof stream.pipe === 'function') {
stream.pipe(duplexStream);
}
if (typeArg === 'nodestream') {
return duplexStream;
}
if (typeArg === 'webstream') {
return (await duplexStream.getWebStreams()).readable;
}
throw new Error('unknown typeArg');
}
/**
* store file as stream
*/
public async fastPutStream(optionsArg: {
path: string;
dataStream: plugins.stream.Readable;
readableStream: plugins.stream.Readable | ReadableStream;
nativeMetadata?: { [key: string]: string };
overwrite?: boolean;
}): Promise<void> {
try {
// Check if the object already exists
const exists = await this.fastExists({ path: optionsArg.path });
if (exists && !optionsArg.overwrite) {
console.error(`Object already exists at path '${optionsArg.path}' in bucket '${this.name}'.`);
console.error(
`Object already exists at path '${optionsArg.path}' in bucket '${this.name}'.`
);
return;
} else if (exists && optionsArg.overwrite) {
console.log(`Overwriting existing object at path '${optionsArg.path}' in bucket '${this.name}'.`);
console.log(
`Overwriting existing object at path '${optionsArg.path}' in bucket '${this.name}'.`
);
} else {
console.log(`Creating new object at path '${optionsArg.path}' in bucket '${this.name}'.`);
}
// Proceed with putting the object
await this.smartbucketRef.minioClient.putObject(
this.name,
optionsArg.path,
optionsArg.dataStream,
null,
...(optionsArg.nativeMetadata
? (() => {
const returnObject: any = {};
return returnObject;
})()
: {})
const command = new plugins.s3.PutObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
Body: optionsArg.readableStream,
Metadata: optionsArg.nativeMetadata,
});
await this.smartbucketRef.s3Client.send(command);
console.log(
`Object '${optionsArg.path}' has been successfully stored in bucket '${this.name}'.`
);
console.log(`Object '${optionsArg.path}' has been successfully stored in bucket '${this.name}'.`);
} catch (error) {
console.error(`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`, error);
console.error(
`Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
error
);
throw error;
}
}
public async fastCopy(optionsArg: {
sourcePath: string;
@ -216,94 +302,107 @@ export class Bucket {
const targetBucketName = optionsArg.targetBucket ? optionsArg.targetBucket.name : this.name;
// Retrieve current object information to use in copy conditions
const currentObjInfo = await this.smartbucketRef.minioClient.statObject(
targetBucketName,
optionsArg.sourcePath
const currentObjInfo = await this.smartbucketRef.s3Client.send(
new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: optionsArg.sourcePath,
})
);
// Setting up copy conditions
const copyConditions = new plugins.minio.CopyConditions();
// Prepare new metadata
const newNativeMetadata = {
...(optionsArg.deleteExistingNativeMetadata ? {} : currentObjInfo.metaData),
...(optionsArg.deleteExistingNativeMetadata ? {} : currentObjInfo.Metadata),
...optionsArg.nativeMetadata,
};
// Define the copy operation as a Promise
// TODO: check on issue here: https://github.com/minio/minio-js/issues/1286
await this.smartbucketRef.minioClient.copyObject(
this.name,
optionsArg.sourcePath,
`/${targetBucketName}/${optionsArg.destinationPath || optionsArg.sourcePath}`,
copyConditions
);
// Define the copy operation
const copySource = `${this.name}/${optionsArg.sourcePath}`;
const command = new plugins.s3.CopyObjectCommand({
Bucket: targetBucketName,
CopySource: copySource,
Key: optionsArg.destinationPath || optionsArg.sourcePath,
Metadata: newNativeMetadata,
MetadataDirective: optionsArg.deleteExistingNativeMetadata ? 'REPLACE' : 'COPY',
});
await this.smartbucketRef.s3Client.send(command);
} catch (err) {
console.error('Error updating metadata:', err);
throw err; // rethrow to allow caller to handle
}
}
/**
/**
* Move object from one path to another within the same bucket or to another bucket
*/
public async fastMove(optionsArg: {
sourcePath: string;
destinationPath: string;
targetBucket?: Bucket;
overwrite?: boolean;
}): Promise<void> {
try {
// Check if the destination object already exists
const destinationBucket = optionsArg.targetBucket || this;
const exists = await destinationBucket.fastExists({ path: optionsArg.destinationPath });
if (exists && !optionsArg.overwrite) {
console.error(`Object already exists at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`);
return;
} else if (exists && optionsArg.overwrite) {
console.log(`Overwriting existing object at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`);
} else {
console.log(`Moving object to path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`);
}
// Proceed with copying the object to the new path
await this.fastCopy(optionsArg);
// Remove the original object after successful copy
await this.fastRemove({ path: optionsArg.sourcePath });
console.log(`Object '${optionsArg.sourcePath}' has been successfully moved to '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`);
} catch (error) {
console.error(`Error moving object from '${optionsArg.sourcePath}' to '${optionsArg.destinationPath}':`, error);
throw error;
public async fastMove(optionsArg: {
sourcePath: string;
destinationPath: string;
targetBucket?: Bucket;
overwrite?: boolean;
}): Promise<void> {
try {
const destinationBucket = optionsArg.targetBucket || this;
const exists = await destinationBucket.fastExists({
path: optionsArg.destinationPath,
});
if (exists && !optionsArg.overwrite) {
console.error(
`Object already exists at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
return;
} else if (exists && optionsArg.overwrite) {
console.log(
`Overwriting existing object at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
} else {
console.log(
`Moving object to path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
}
await this.fastCopy(optionsArg);
await this.fastRemove({ path: optionsArg.sourcePath });
console.log(
`Object '${optionsArg.sourcePath}' has been successfully moved to '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
);
} catch (error) {
console.error(
`Error moving object from '${optionsArg.sourcePath}' to '${optionsArg.destinationPath}':`,
error
);
throw error;
}
}
/**
* removeObject
*/
public async fastRemove(optionsArg: {
path: string;
}) {
await this.smartbucketRef.minioClient.removeObject(this.name, optionsArg.path);
public async fastRemove(optionsArg: { path: string }) {
const command = new plugins.s3.DeleteObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
await this.smartbucketRef.s3Client.send(command);
}
/**
* check wether file exists
* check whether file exists
* @param optionsArg
* @returns
* @returns
*/
public async fastExists(optionsArg: {
path: string;
}): Promise<boolean> {
public async fastExists(optionsArg: { path: string }): Promise<boolean> {
try {
await this.smartbucketRef.minioClient.statObject(this.name, optionsArg.path);
const command = new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: optionsArg.path,
});
await this.smartbucketRef.s3Client.send(command);
console.log(`Object '${optionsArg.path}' exists in bucket '${this.name}'.`);
return true;
} catch (error) {
if (error.code === 'NotFound') {
} catch (error: any) {
if (error?.name === 'NotFound') {
console.log(`Object '${optionsArg.path}' does not exist in bucket '${this.name}'.`);
return false;
} else {
@ -313,56 +412,115 @@ export class Bucket {
}
}
/**
* deletes this bucket
*/
public async delete() {
await this.smartbucketRef.s3Client.send(
new plugins.s3.DeleteBucketCommand({ Bucket: this.name })
);
}
public async fastStat(pathDescriptor: interfaces.IPathDecriptor) {
let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
return this.smartbucketRef.minioClient.statObject(this.name, checkPath);
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.HeadObjectCommand({
Bucket: this.name,
Key: checkPath,
});
return this.smartbucketRef.s3Client.send(command);
}
public async isDirectory(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
// lets check if the checkPath is a directory
const stream = this.smartbucketRef.minioClient.listObjectsV2(this.name, checkPath, true);
const done = plugins.smartpromise.defer<boolean>();
stream.on('data', (dataArg) => {
stream.destroy(); // Stop the stream early if we find at least one object
if (dataArg.prefix.startsWith(checkPath + '/')) {
done.resolve(true);
}
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.name,
Prefix: checkPath,
Delimiter: '/',
});
stream.on('end', () => {
done.resolve(false);
});
stream.on('error', (err) => {
done.reject(err);
});
return done.promise;
};
const { CommonPrefixes } = await this.smartbucketRef.s3Client.send(command);
return !!CommonPrefixes && CommonPrefixes.length > 0;
}
public async isFile(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
// lets check if the checkPath is a directory
const stream = this.smartbucketRef.minioClient.listObjectsV2(this.name, checkPath, true);
const done = plugins.smartpromise.defer<boolean>();
stream.on('data', (dataArg) => {
stream.destroy(); // Stop the stream early if we find at least one object
if (dataArg.prefix === checkPath) {
done.resolve(true);
}
});
stream.on('end', () => {
done.resolve(false);
});
stream.on('error', (err) => {
done.reject(err);
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.name,
Prefix: checkPath,
Delimiter: '/',
});
const { Contents } = await this.smartbucketRef.s3Client.send(command);
return !!Contents && Contents.length > 0;
}
return done.promise;
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;
}
}
public async cleanAllContents(): Promise<void> {
try {
// Define the command type explicitly
const listCommandInput: plugins.s3.ListObjectsV2CommandInput = {
Bucket: this.name,
};
let isTruncated = true;
let continuationToken: string | undefined = undefined;
while (isTruncated) {
// Add the continuation token to the input if present
const listCommand = new plugins.s3.ListObjectsV2Command({
...listCommandInput,
ContinuationToken: continuationToken,
});
// Explicitly type the response
const response: plugins.s3.ListObjectsV2Output =
await this.smartbucketRef.s3Client.send(listCommand);
console.log(`Cleaning contents of bucket '${this.name}': Now deleting ${response.Contents?.length} items...`);
if (response.Contents && response.Contents.length > 0) {
// Delete objects in batches, mapping each item to { Key: string }
const deleteCommand = new plugins.s3.DeleteObjectsCommand({
Bucket: this.name,
Delete: {
Objects: response.Contents.map((item) => ({ Key: item.Key! })),
Quiet: true,
},
});
await this.smartbucketRef.s3Client.send(deleteCommand);
}
// Update continuation token and truncation status
isTruncated = response.IsTruncated || false;
continuationToken = response.NextContinuationToken;
}
console.log(`All contents in bucket '${this.name}' have been deleted.`);
} catch (error) {
console.error(`Error cleaning contents of bucket '${this.name}':`, error);
throw error;
}
}
}

View File

@ -1,6 +1,9 @@
// classes.directory.ts
import * as plugins from './plugins.js';
import { Bucket } from './classes.bucket.js';
import { File } from './classes.file.js';
import * as helpers from './helpers.js';
export class Directory {
public bucketRef: Bucket;
@ -11,9 +14,9 @@ export class Directory {
public files: string[];
public folders: string[];
constructor(bucketRefArg: Bucket, parentDiretory: Directory, name: string) {
constructor(bucketRefArg: Bucket, parentDirectory: Directory, name: string) {
this.bucketRef = bucketRefArg;
this.parentDirectoryRef = parentDiretory;
this.parentDirectoryRef = parentDirectory;
this.name = name;
}
@ -63,52 +66,66 @@ export class Directory {
* gets a file by name
*/
public async getFile(optionsArg: {
name: string;
path: string;
createWithContents?: string | Buffer;
}): Promise<File> {
// check wether the file exists
getFromTrash?: boolean;
}): Promise<File | null> {
const pathDescriptor = {
directory: this,
path: optionsArg.path,
};
const exists = await this.bucketRef.fastExists({
path: this.getBasePath() + optionsArg.name,
path: await helpers.reducePathDescriptorToPath(pathDescriptor),
});
if (!exists && optionsArg.getFromTrash) {
const trash = await this.bucketRef.getTrash();
const trashedFile = await trash.getTrashedFileByOriginalName(pathDescriptor);
return trashedFile;
}
if (!exists && !optionsArg.createWithContents) {
return null;
}
if (!exists && optionsArg.createWithContents) {
await this.fastPut({
path: optionsArg.name,
await File.create({
directory: this,
name: optionsArg.path,
contents: optionsArg.createWithContents,
});
}
return new File({
directoryRefArg: this,
fileName: optionsArg.name,
})
fileName: optionsArg.path,
});
}
/**
* gets a file strictly
* @param args
* @returns
*/
public async getFileStrict(...args: Parameters<Directory['getFile']>) {
const file = await this.getFile(...args);
if (!file) {
throw new Error(`File not found at path '${args[0].path}'`);
}
return file;
}
/**
* lists all files
*/
public async listFiles(): Promise<File[]> {
const done = plugins.smartpromise.defer();
const fileNameStream = await this.bucketRef.smartbucketRef.minioClient.listObjectsV2(
this.bucketRef.name,
this.getBasePath(),
false
);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.bucketRef.name,
Prefix: this.getBasePath(),
Delimiter: '/',
});
const response = await this.bucketRef.smartbucketRef.s3Client.send(command);
const fileArray: File[] = [];
const duplexStream = new plugins.smartstream.SmartDuplex<plugins.minio.BucketItem, void>({
objectMode: true,
writeFunction: async (bucketItem) => {
if (bucketItem.prefix) {
return;
}
if (!bucketItem.name) {
return;
}
let subtractedPath = bucketItem.name.replace(this.getBasePath(), '');
if (subtractedPath.startsWith('/')) {
subtractedPath = subtractedPath.substr(1);
}
response.Contents?.forEach((item) => {
if (item.Key && !item.Key.endsWith('/')) {
const subtractedPath = item.Key.replace(this.getBasePath(), '');
if (!subtractedPath.includes('/')) {
fileArray.push(
new File({
@ -117,13 +134,9 @@ export class Directory {
})
);
}
},
finalFunction: async (tools) => {
done.resolve();
}
});
fileNameStream.pipe(duplexStream);
await done.promise;
return fileArray;
}
@ -131,70 +144,100 @@ export class Directory {
* lists all folders
*/
public async listDirectories(): Promise<Directory[]> {
const done = plugins.smartpromise.defer();
const basePath = this.getBasePath();
const completeDirStream = await this.bucketRef.smartbucketRef.minioClient.listObjectsV2(
this.bucketRef.name,
this.getBasePath(),
false
);
const directoryArray: Directory[] = [];
const duplexStream = new plugins.smartstream.SmartDuplex<plugins.minio.BucketItem, void>({
objectMode: true,
writeFunction: async (bucketItem) => {
if (bucketItem.name) {
return;
}
let subtractedPath = bucketItem.prefix.replace(this.getBasePath(), '');
if (subtractedPath.startsWith('/')) {
subtractedPath = subtractedPath.substr(1);
}
if (subtractedPath.includes('/')) {
const dirName = subtractedPath.split('/')[0];
if (directoryArray.find((directory) => directory.name === dirName)) {
return;
try {
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.bucketRef.name,
Prefix: this.getBasePath(),
Delimiter: '/',
});
const response = await this.bucketRef.smartbucketRef.s3Client.send(command);
const directoryArray: Directory[] = [];
if (response.CommonPrefixes) {
response.CommonPrefixes.forEach((item) => {
if (item.Prefix) {
const subtractedPath = item.Prefix.replace(this.getBasePath(), '');
if (subtractedPath.endsWith('/')) {
const dirName = subtractedPath.slice(0, -1);
// Ensure the directory name is not empty (which would indicate the base directory itself)
if (dirName) {
directoryArray.push(new Directory(this.bucketRef, this, dirName));
}
}
}
directoryArray.push(new Directory(this.bucketRef, this, dirName));
}
},
finalFunction: async (tools) => {
done.resolve();
});
}
});
completeDirStream.pipe(duplexStream);
await done.promise;
return directoryArray;
return directoryArray;
} catch (error) {
console.error('Error listing directories:', error);
throw error;
}
}
/**
* gets an array that has all objects with a certain prefix;
* gets an array that has all objects with a certain prefix
*/
public async getTreeArray() {
const treeArray = await this.bucketRef.smartbucketRef.minioClient.listObjectsV2(
this.bucketRef.name,
this.getBasePath(),
true
);
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.bucketRef.name,
Prefix: this.getBasePath(),
Delimiter: '/',
});
const response = await this.bucketRef.smartbucketRef.s3Client.send(command);
return response.Contents;
}
/**
* gets a sub directory
* gets a sub directory by name
*/
public async getSubDirectoryByName(dirNameArg: string): Promise<Directory> {
const dirNameArray = dirNameArg.split('/');
public async getSubDirectoryByName(dirNameArg: string, optionsArg: {
getEmptyDirectory?: boolean;
createWithInitializerFile?: boolean;
} = {}): Promise<Directory | null> {
const dirNameArray = dirNameArg.split('/').filter(str => str.trim() !== "");
const getDirectory = async (directoryArg: Directory, dirNameToSearch: string) => {
optionsArg = {
getEmptyDirectory: false,
createWithInitializerFile: false,
...optionsArg,
}
const getDirectory = async (directoryArg: Directory, dirNameToSearch: string, isFinalDirectory: boolean) => {
const directories = await directoryArg.listDirectories();
return directories.find((directory) => {
let returnDirectory = directories.find((directory) => {
return directory.name === dirNameToSearch;
});
if (returnDirectory) {
return returnDirectory;
}
if (optionsArg.getEmptyDirectory || optionsArg.createWithInitializerFile) {
returnDirectory = new Directory(this.bucketRef, this, dirNameToSearch);
}
if (isFinalDirectory && optionsArg.createWithInitializerFile) {
returnDirectory?.createEmptyFile('00init.txt');
}
return returnDirectory || null;
};
let wantedDirectory: Directory;
let wantedDirectory: Directory | null = null;
let counter = 0;
for (const dirNameToSearch of dirNameArray) {
counter++;
const directoryToSearchIn = wantedDirectory ? wantedDirectory : this;
wantedDirectory = await getDirectory(directoryToSearchIn, dirNameToSearch);
wantedDirectory = await getDirectory(directoryToSearchIn, dirNameToSearch, counter === dirNameArray.length);
}
return wantedDirectory;
return wantedDirectory || null;
}
public async getSubDirectoryByNameStrict(...args: Parameters<Directory['getSubDirectoryByName']>) {
const directory = await this.getSubDirectoryByName(...args);
if (!directory) {
throw new Error(`Directory not found at path '${args[0]}'`);
}
return directory;
}
/**
@ -202,19 +245,20 @@ export class Directory {
*/
public async move() {
// TODO
throw new Error('moving a directory is not yet implemented');
throw new Error('Moving a directory is not yet implemented');
}
/**
* creates a file within this directory
* creates an empty file within this directory
* @param relativePathArg
*/
public async createEmptyFile(relativePathArg: string) {
const emtpyFile = await File.create({
const emptyFile = await File.create({
directory: this,
name: relativePathArg,
contents: '',
});
return emptyFile;
}
// file operations
@ -234,29 +278,82 @@ export class Directory {
return result;
}
public async fastGetStream(pathArg: string): Promise<plugins.smartrx.rxjs.ReplaySubject<Buffer>> {
const path = plugins.path.join(this.getBasePath(), pathArg);
const result = await this.bucketRef.fastGetStream({
path,
});
public fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'webstream'
): Promise<ReadableStream>;
public async fastGetStream(
optionsArg: {
path: string;
},
typeArg: 'nodestream'
): Promise<plugins.stream.Readable>;
/**
* fastGetStream
* @param optionsArg
* @returns
*/
public async fastGetStream(
optionsArg: { path: string },
typeArg: 'webstream' | 'nodestream'
): Promise<ReadableStream | plugins.stream.Readable> {
const path = plugins.path.join(this.getBasePath(), optionsArg.path);
const result = await this.bucketRef.fastGetStream(
{
path,
},
typeArg as any
);
return result;
}
public async fastRemove(optionsArg: { path: string }) {
/**
* fast put stream
*/
public async fastPutStream(optionsArg: {
path: string;
stream: plugins.stream.Readable;
}): Promise<void> {
const path = plugins.path.join(this.getBasePath(), optionsArg.path);
await this.bucketRef.fastRemove({
await this.bucketRef.fastPutStream({
path,
readableStream: optionsArg.stream,
});
}
/**
* removes a file within the directory
* uses file class to make sure effects for metadata etc. are handled correctly
* @param optionsArg
*/
public async fastRemove(optionsArg: {
path: string
/**
* 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
*/
public async delete() {
public async delete(optionsArg: {
mode?: 'permanent' | 'trash';
}) {
const deleteDirectory = async (directoryArg: Directory) => {
const childDirectories = await directoryArg.listDirectories();
if (childDirectories.length === 0) {
console.log('directory empty! Path complete!');
console.log('Directory empty! Path complete!');
} else {
for (const childDir of childDirectories) {
await deleteDirectory(childDir);
@ -264,9 +361,9 @@ export class Directory {
}
const files = await directoryArg.listFiles();
for (const file of files) {
await directoryArg.fastRemove({
path: file.name,
});
await file.delete({
mode: optionsArg.mode ? optionsArg.mode : 'permanent',
})
}
};
await deleteDirectory(this);

View File

@ -4,7 +4,6 @@ import * as interfaces from './interfaces.js';
import { Directory } from './classes.directory.js';
import { MetaData } from './classes.metadata.js';
/**
* represents a file in a directory
*/
@ -33,7 +32,12 @@ export class File {
directoryRefArg: optionsArg.directory,
fileName: optionsArg.name,
});
if (contents instanceof plugins.stream.Readable) {} else {
if (contents instanceof plugins.stream.Readable) {
await optionsArg.directory.fastPutStream({
path: optionsArg.name,
stream: contents,
});
} else {
await optionsArg.directory.fastPut({
path: optionsArg.name,
contents: contents,
@ -46,9 +50,13 @@ export class File {
public parentDirectoryRef: Directory;
public name: string;
/**
* get the full path to the file
* @returns the full path to the file
*/
public getBasePath(): string {
return plugins.path.join(this.parentDirectoryRef.getBasePath(), this.name);
};
}
constructor(optionsArg: { directoryRefArg: Directory; fileName: string }) {
this.parentDirectoryRef = optionsArg.directoryRefArg;
@ -67,33 +75,59 @@ export class File {
return resultBuffer;
}
public async getReadStream() {
const readStream = this.parentDirectoryRef.bucketRef.fastGetStream({
path: this.getBasePath(),
});
public async getReadStream(typeArg: 'webstream'): Promise<ReadableStream>;
public async getReadStream(typeArg: 'nodestream'): Promise<plugins.stream.Readable>;
public async getReadStream(
typeArg: 'nodestream' | 'webstream'
): Promise<ReadableStream | plugins.stream.Readable> {
const readStream = this.parentDirectoryRef.bucketRef.fastGetStream(
{
path: this.getBasePath(),
},
typeArg as any
);
return readStream;
}
/**
* removes this file
* for using recycling mechanics use .delete()
* deletes this file
*/
public async remove() {
await this.parentDirectoryRef.bucketRef.fastRemove({
path: this.getBasePath(),
});
if (!this.name.endsWith('.metadata')) {
public async delete(optionsArg?: { mode: 'trash' | 'permanent' }) {
optionsArg = {
...{
mode: 'permanent',
},
...optionsArg,
};
if (optionsArg.mode === 'permanent') {
await this.parentDirectoryRef.bucketRef.fastRemove({
path: this.getBasePath() + '.metadata',
path: this.getBasePath(),
});
if (!this.name.endsWith('.metadata')) {
if (await this.hasMetaData()) {
const metadata = await this.getMetaData();
await metadata.metadataFile.delete(optionsArg);
}
}
} else if (optionsArg.mode === 'trash') {
const metadata = await this.getMetaData();
await metadata.storeCustomMetaData({
key: 'recycle',
value: {
deletedAt: Date.now(),
originalPath: this.getBasePath(),
},
});
const trash = await this.parentDirectoryRef.bucketRef.getTrash();
const trashDir = await trash.getTrashDir();
await this.move({
directory: trashDir,
path: await trash.getTrashKeyByOriginalBasePath(this.getBasePath()),
});
}
await this.parentDirectoryRef.listFiles();
}
/**
* deletes the file with recycling mechanics
*/
public async delete() {
await this.remove();
await this.parentDirectoryRef.listFiles();
}
/**
@ -125,23 +159,29 @@ export class File {
}
public async updateWithContents(optionsArg: {
contents: Buffer | string | plugins.stream.Readable;
contents: Buffer | string | plugins.stream.Readable | ReadableStream;
encoding?: 'utf8' | 'binary';
}) {
if (optionsArg.contents instanceof plugins.stream.Readable) {
if (
optionsArg.contents instanceof plugins.stream.Readable ||
optionsArg.contents instanceof ReadableStream
) {
await this.parentDirectoryRef.bucketRef.fastPutStream({
path: this.getBasePath(),
dataStream: optionsArg.contents,
readableStream: optionsArg.contents,
overwrite: true,
});
} else if (Buffer.isBuffer(optionsArg.contents)) {
await this.parentDirectoryRef.bucketRef.fastPut({
path: this.getBasePath(),
contents: optionsArg.contents,
overwrite: true,
});
} else if (typeof optionsArg.contents === 'string') {
await this.parentDirectoryRef.bucketRef.fastPut({
path: this.getBasePath(),
contents: Buffer.from(optionsArg.contents, optionsArg.encoding),
overwrite: true,
});
}
}
@ -150,23 +190,49 @@ export class File {
* moves the file to another directory
*/
public async move(pathDescriptorArg: interfaces.IPathDecriptor) {
let moveToPath = '';
let moveToPath: string = '';
const isDirectory = await this.parentDirectoryRef.bucketRef.isDirectory(pathDescriptorArg);
if (isDirectory) {
moveToPath = await helpers.reducePathDescriptorToPath({
...pathDescriptorArg,
path: plugins.path.join(pathDescriptorArg.path, this.name),
path: plugins.path.join(pathDescriptorArg.path!, this.name),
});
} else {
moveToPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
}
// lets move the file
await this.parentDirectoryRef.bucketRef.fastMove({
sourcePath: this.getBasePath(),
destinationPath: moveToPath,
overwrite: true,
});
// lets move the metadatafile
const metadata = await this.getMetaData();
await metadata.metadataFile.move(pathDescriptorArg);
if (!this.name.endsWith('.metadata')) {
const metadata = await this.getMetaData();
await this.parentDirectoryRef.bucketRef.fastMove({
sourcePath: metadata.metadataFile.getBasePath(),
destinationPath: moveToPath + '.metadata',
overwrite: true,
});
}
// lets update references of this
const baseDirectory = await this.parentDirectoryRef.bucketRef.getBaseDirectory();
this.parentDirectoryRef = await baseDirectory.getSubDirectoryByNameStrict(
pathDescriptorArg.directory?.getBasePath()!
);
this.name = pathDescriptorArg.path!;
}
public async hasMetaData(): Promise<boolean> {
if (!this.name.endsWith('.metadata')) {
const hasMetadataBool = MetaData.hasMetaData({
file: this,
});
return hasMetadataBool;
} else {
return false;
}
}
/**
@ -197,4 +263,11 @@ export class File {
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

@ -3,14 +3,22 @@ import * as plugins from './plugins.js';
import { File } from './classes.file.js';
export class MetaData {
public static async hasMetaData(optionsArg: { file: File }) {
// lets find the existing metadata file
const existingFile = await optionsArg.file.parentDirectoryRef.getFile({
path: optionsArg.file.name + '.metadata',
});
return !!existingFile;
}
// static
public static async createForFile(optionsArg: { file: File }) {
const metaData = new MetaData();
metaData.fileRef = optionsArg.file;
// lets find the existing metadata file
metaData.metadataFile = await metaData.fileRef.parentDirectoryRef.getFile({
name: metaData.fileRef.name + '.metadata',
metaData.metadataFile = await metaData.fileRef.parentDirectoryRef.getFileStrict({
path: metaData.fileRef.name + '.metadata',
createWithContents: '{}',
});
@ -21,20 +29,34 @@ export class MetaData {
/**
* the file that contains the metadata
*/
metadataFile: File;
metadataFile!: File;
/**
* the file that the metadata is for
*/
fileRef: File;
fileRef!: File;
public async getFileType(optionsArg?: {
useFileExtension?: boolean;
useMagicBytes?: boolean;
}): Promise<string> {
if ((optionsArg && optionsArg.useFileExtension) || optionsArg.useFileExtension === undefined) {
return plugins.path.extname(this.fileRef.name);
}): Promise<plugins.smartmime.IFileTypeResult | undefined> {
if ((optionsArg && optionsArg.useFileExtension) || !optionsArg) {
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 +66,13 @@ export class MetaData {
const stat = await this.fileRef.parentDirectoryRef.bucketRef.fastStat({
path: this.fileRef.getBasePath(),
});
return stat.size;
return stat.ContentLength!;
}
private prefixCustomMetaData = 'custom_';
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;
await this.metadataFile.writeJsonData(data);
}

View File

@ -1,22 +1,34 @@
// classes.smartbucket.ts
import * as plugins from './plugins.js';
import { Bucket } from './classes.bucket.js';
export class SmartBucket {
public config: plugins.tsclass.storage.IS3Descriptor;
public minioClient: plugins.minio.Client;
public s3Client: plugins.s3.S3Client;
/**
* the constructor of SmartBucket
*/
/**
* the constructor of SmartBucket
*/
constructor(configArg: plugins.tsclass.storage.IS3Descriptor) {
this.config = configArg;
this.minioClient = new plugins.minio.Client({
endPoint: this.config.endpoint,
port: configArg.port || 443,
useSSL: configArg.useSsl !== undefined ? configArg.useSsl : true,
accessKey: this.config.accessKey,
secretKey: this.config.accessSecret,
const protocol = configArg.useSsl === false ? 'http' : 'https';
const port = configArg.port ? `:${configArg.port}` : '';
const endpoint = `${protocol}://${configArg.endpoint}${port}`;
this.s3Client = new plugins.s3.S3Client({
endpoint,
region: configArg.region || 'us-east-1',
credentials: {
accessKeyId: configArg.accessKey,
secretAccessKey: configArg.accessSecret,
},
forcePathStyle: true, // Necessary for S3-compatible storage like MinIO or Wasabi
});
}
@ -29,7 +41,15 @@ export class SmartBucket {
await Bucket.removeBucketByName(this, bucketName);
}
public async getBucketByName(bucketName: string) {
return Bucket.getBucketByName(this, bucketName);
public async getBucketByName(bucketNameArg: string) {
return Bucket.getBucketByName(this, bucketNameArg);
}
public async getBucketByNameStrict(...args: Parameters<SmartBucket['getBucketByName']>) {
const bucket = await this.getBucketByName(...args);
if (!bucket) {
throw new Error(`Bucket ${args[0]} does not exist.`);
}
return bucket;
}
}

30
ts/classes.trash.ts Normal file
View File

@ -0,0 +1,30 @@
import * as plugins from './plugins.js';
import * as interfaces from './interfaces.js';
import * as helpers from './helpers.js';
import type { Bucket } from './classes.bucket.js';
import type { Directory } from './classes.directory.js';
import type { File } from './classes.file.js';
export class Trash {
public bucketRef: Bucket;
constructor(bucketRefArg: Bucket) {
this.bucketRef = bucketRefArg;
}
public async getTrashDir() {
return this.bucketRef.getDirectoryFromPath({ path: '.trash' });
}
public async getTrashedFileByOriginalName(pathDescriptor: interfaces.IPathDecriptor): Promise<File> {
const trashDir = await this.getTrashDir();
const originalPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
const trashKey = await this.getTrashKeyByOriginalBasePath(originalPath);
return trashDir.getFileStrict({ path: trashKey });
}
public async getTrashKeyByOriginalBasePath (originalPath: string): Promise<string> {
return plugins.smartstring.base64.encode(originalPath);
}
}

View File

@ -1,3 +1,5 @@
// plugins.ts
// node native
import * as path from 'path';
import * as stream from 'stream';
@ -10,8 +12,10 @@ import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrx from '@push.rocks/smartrx';
import * as smartstream from '@push.rocks/smartstream';
import * as smartstring from '@push.rocks/smartstring';
import * as smartunique from '@push.rocks/smartunique';
export { smartmime, smartpath, smartpromise, smartrx, smartstream };
export { smartmime, smartpath, smartpromise, smartrx, smartstream, smartstring, smartunique };
// @tsclass
import * as tsclass from '@tsclass/tsclass';
@ -21,6 +25,8 @@ export {
}
// third party scope
import * as minio from 'minio';
import * as s3 from '@aws-sdk/client-s3';
export { minio };
export {
s3,
}

View File

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