Compare commits

...

6 Commits

Author SHA1 Message Date
1a71c76da3 3.0.7 2024-05-27 12:56:26 +02:00
e924511147 fix(s3 paths): pathing differences now correctly handled in a reducePath method. 2024-05-27 12:56:25 +02:00
645ebbdd4d 3.0.6 2024-05-21 18:47:00 +02:00
168148b2c9 fix(core): update 2024-05-21 18:46:59 +02:00
1293fc4ca6 3.0.5 2024-05-21 18:42:55 +02:00
b040120813 fix(core): update 2024-05-21 18:42:55 +02:00
11 changed files with 266 additions and 84 deletions

View File

@ -8,7 +8,7 @@
"githost": "code.foss.global", "githost": "code.foss.global",
"gitscope": "push.rocks", "gitscope": "push.rocks",
"gitrepo": "smartbucket", "gitrepo": "smartbucket",
"description": "A TypeScript library that offers simple, cloud-independent object storage with features like bucket creation, file management, and directory management.", "description": "A TypeScript library for cloud-independent object storage, providing features like bucket creation, file and directory management, and data streaming.",
"npmPackagename": "@push.rocks/smartbucket", "npmPackagename": "@push.rocks/smartbucket",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
@ -26,7 +26,12 @@
"unified storage", "unified storage",
"buffer handling", "buffer handling",
"access key", "access key",
"secret key" "secret key",
"metadata",
"file locking",
"file streaming",
"directory listing",
"cloud agnostic"
] ]
} }
}, },

4
package-lock.json generated
View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "@push.rocks/smartbucket", "name": "@push.rocks/smartbucket",
"version": "3.0.4", "version": "3.0.7",
"description": "A TypeScript library that offers simple, cloud-independent object storage with features like bucket creation, file management, and directory management.", "description": "A TypeScript library for cloud-independent object storage, providing 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",
"type": "module", "type": "module",
@ -58,6 +58,11 @@
"unified storage", "unified storage",
"buffer handling", "buffer handling",
"access key", "access key",
"secret key" "secret key",
"metadata",
"file locking",
"file streaming",
"directory listing",
"cloud agnostic"
] ]
} }

View File

@ -1,5 +1,6 @@
# @push.rocks/smartbucket # @push.rocks/smartbucket
A TypeScript library for simple cloud independent object storage with support for buckets, directories, and files.
A TypeScript library that offers simple, cloud-independent object storage with features like bucket creation, file management, and directory management.
## Install ## Install
@ -199,12 +200,15 @@ writeFileStream("exampleBucket", "path/to/streamedObject.txt", readable);
`@push.rocks/smartbucket` abstracts directories within buckets for easier object management. You can create, list, and delete directories using the `Directory` class. `@push.rocks/smartbucket` abstracts directories within buckets for easier object management. You can create, list, and delete directories using the `Directory` class.
Here's how to list the contents of a directory:
```typescript ```typescript
async function listDirectoryContents(bucketName: string, directoryPath: string) { async function listDirectoryContents(bucketName: string, directoryPath: string) {
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName); const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
if (myBucket) { if (myBucket) {
const baseDirectory: Directory = await myBucket.getBaseDirectory(); const baseDirectory: Directory = await myBucket.getBaseDirectory();
const targetDirectory: Directory = await baseDirectory.getSubDirectoryByName(directoryPath); const targetDirectory: Directory = await baseDirectory.getSubDirectoryByName(directoryPath);
console.log('Listing directories:'); console.log('Listing directories:');
const directories = await targetDirectory.listDirectories(); const directories = await targetDirectory.listDirectories();
directories.forEach(dir => { directories.forEach(dir => {
@ -304,6 +308,8 @@ Remember, each cloud provider has specific features and limitations. `@push.rock
This guide covers the basic to advanced scenarios of using `@push.rocks/smartbucket`. For further details, refer to the API documentation and examples. This guide covers the basic to advanced scenarios of using `@push.rocks/smartbucket`. For further details, refer to the API documentation and examples.
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.

View File

@ -79,7 +79,7 @@ tap.test('prepare for directory style tests', async () => {
contents: 'dir3/dir4/file1.txt content', contents: 'dir3/dir4/file1.txt content',
}); });
await myBucket.fastPut({ await myBucket.fastPut({
path: 'file1.txt', path: '/file1.txt',
contents: 'file1 content', contents: 'file1 content',
}); });
}); });

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartbucket', name: '@push.rocks/smartbucket',
version: '3.0.4', version: '3.0.7',
description: 'A TypeScript library that offers simple, cloud-independent object storage with features like bucket creation, file management, and directory management.' description: 'A TypeScript library for cloud-independent object storage, providing features like bucket creation, file and directory management, and data streaming.'
} }

View File

@ -1,6 +1,9 @@
import * as plugins from './plugins.js'; 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 { SmartBucket } from './classes.smartbucket.js';
import { Directory } from './classes.directory.js'; import { Directory } from './classes.directory.js';
import { File } from './classes.file.js';
export class Bucket { export class Bucket {
public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) { public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) {
@ -38,10 +41,19 @@ export class Bucket {
/** /**
* gets the base directory of the bucket * gets the base directory of the bucket
*/ */
public async getBaseDirectory() { public async getBaseDirectory(): Promise<Directory> {
return new Directory(this, null, ''); return new Directory(this, null, '');
} }
public async getDirectoryFromPath(pathDescriptorArg: interfaces.IPathDecriptor): Promise<Directory> {
if (!pathDescriptorArg.path && !pathDescriptorArg.directory) {
return this.getBaseDirectory();
}
let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
const baseDirectory = await this.getBaseDirectory();
return await baseDirectory.getSubDirectoryByName(checkPath);
}
// =============== // ===============
// Fast Operations // Fast Operations
// =============== // ===============
@ -53,28 +65,38 @@ export class Bucket {
path: string; path: string;
contents: string | Buffer; contents: string | Buffer;
overwrite?: boolean; overwrite?: boolean;
}): Promise<void> { }): Promise<File> {
try { try {
const reducedPath = await helpers.reducePathDescriptorToPath({
path: optionsArg.path,
})
// Check if the object already exists // Check if the object already exists
const exists = await this.fastExists({ path: optionsArg.path }); const exists = await this.fastExists({ path: reducedPath });
if (exists && !optionsArg.overwrite) { if (exists && !optionsArg.overwrite) {
console.error(`Object already exists at path '${optionsArg.path}' in bucket '${this.name}'.`); console.error(`Object already exists at path '${reducedPath}' in bucket '${this.name}'.`);
return; return;
} else if (exists && optionsArg.overwrite) { } 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 '${reducedPath}' in bucket '${this.name}'.`);
} else { } else {
console.log(`Creating new object at path '${optionsArg.path}' in bucket '${this.name}'.`); console.log(`Creating new object at path '${reducedPath}' in bucket '${this.name}'.`);
} }
// Proceed with putting the object // Proceed with putting the object
const streamIntake = new plugins.smartstream.StreamIntake(); const streamIntake = new plugins.smartstream.StreamIntake();
const putPromise = this.smartbucketRef.minioClient.putObject(this.name, optionsArg.path, streamIntake); const putPromise = this.smartbucketRef.minioClient.putObject(this.name, reducedPath, streamIntake);
streamIntake.pushData(optionsArg.contents); streamIntake.pushData(optionsArg.contents);
streamIntake.signalEnd(); streamIntake.signalEnd();
await putPromise; await putPromise;
console.log(`Object '${optionsArg.path}' has been successfully stored in bucket '${this.name}'.`); console.log(`Object '${reducedPath}' has been successfully stored in bucket '${this.name}'.`);
const parsedPath = plugins.path.parse(reducedPath);
return new File({
directoryRefArg: await this.getDirectoryFromPath({
path: parsedPath.dir,
}),
fileName: parsedPath.base,
});
} catch (error) { } 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; throw error;
@ -183,19 +205,10 @@ export class Bucket {
} }
public async copyObject(optionsArg: { public async fastCopy(optionsArg: {
/** sourcePath: string;
* the destinationPath?: string;
*/
objectKey: string;
/**
* in case you want to copy to another bucket specify it here
*/
targetBucket?: Bucket; targetBucket?: Bucket;
targetBucketKey?: string;
/**
* metadata will be merged with existing metadata
*/
nativeMetadata?: { [key: string]: string }; nativeMetadata?: { [key: string]: string };
deleteExistingNativeMetadata?: boolean; deleteExistingNativeMetadata?: boolean;
}): Promise<void> { }): Promise<void> {
@ -205,7 +218,7 @@ export class Bucket {
// Retrieve current object information to use in copy conditions // Retrieve current object information to use in copy conditions
const currentObjInfo = await this.smartbucketRef.minioClient.statObject( const currentObjInfo = await this.smartbucketRef.minioClient.statObject(
targetBucketName, targetBucketName,
optionsArg.objectKey optionsArg.sourcePath
); );
// Setting up copy conditions // Setting up copy conditions
@ -221,8 +234,8 @@ export class Bucket {
// TODO: check on issue here: https://github.com/minio/minio-js/issues/1286 // TODO: check on issue here: https://github.com/minio/minio-js/issues/1286
await this.smartbucketRef.minioClient.copyObject( await this.smartbucketRef.minioClient.copyObject(
this.name, this.name,
optionsArg.objectKey, optionsArg.sourcePath,
`/${targetBucketName}/${optionsArg.objectKey}`, `/${targetBucketName}/${optionsArg.destinationPath || optionsArg.sourcePath}`,
copyConditions copyConditions
); );
} catch (err) { } catch (err) {
@ -231,6 +244,43 @@ export class Bucket {
} }
} }
/**
* 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;
}
}
/** /**
* removeObject * removeObject
*/ */
@ -263,9 +313,56 @@ export class Bucket {
} }
} }
public async fastStat(optionsArg: { public async fastStat(pathDescriptor: interfaces.IPathDecriptor) {
path: string; let checkPath = await helpers.reducePathDescriptorToPath(pathDescriptor);
}) { return this.smartbucketRef.minioClient.statObject(this.name, checkPath);
return this.smartbucketRef.minioClient.statObject(this.name, optionsArg.path); }
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);
}
});
stream.on('end', () => {
done.resolve(false);
});
stream.on('error', (err) => {
done.reject(err);
});
return done.promise;
};
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);
});
return done.promise;
} }
} }

View File

@ -1,4 +1,6 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import * as helpers from './helpers.js';
import * as interfaces from './interfaces.js';
import { Directory } from './classes.directory.js'; import { Directory } from './classes.directory.js';
import { MetaData } from './classes.metadata.js'; import { MetaData } from './classes.metadata.js';
@ -102,7 +104,7 @@ export class File {
const metadata = await this.getMetaData(); const metadata = await this.getMetaData();
await metadata.setLock({ await metadata.setLock({
lock: 'locked', lock: 'locked',
expires: new Date(Date.now() + (optionsArg?.timeoutMillis || 1000)), expires: Date.now() + (optionsArg?.timeoutMillis || 1000),
}); });
} }
@ -116,7 +118,10 @@ export class File {
*/ */
force?: boolean; force?: boolean;
}) { }) {
const metadata = await this.getMetaData();
await metadata.removeLock({
force: optionsArg?.force,
});
} }
public async updateWithContents(optionsArg: { public async updateWithContents(optionsArg: {
@ -141,14 +146,55 @@ export class File {
} }
} }
/**
* moves the file to another directory
*/
public async move(pathDescriptorArg: interfaces.IPathDecriptor) {
let moveToPath = '';
const isDirectory = await this.parentDirectoryRef.bucketRef.isDirectory(pathDescriptorArg);
if (isDirectory) {
moveToPath = await helpers.reducePathDescriptorToPath({
...pathDescriptorArg,
path: plugins.path.join(pathDescriptorArg.path, this.name),
});
}
// lets move the file
await this.parentDirectoryRef.bucketRef.fastMove({
sourcePath: this.getBasePath(),
destinationPath: moveToPath,
});
// lets move the metadatafile
const metadata = await this.getMetaData();
await metadata.metadataFile.move(pathDescriptorArg);
}
/** /**
* allows updating the metadata of a file * allows updating the metadata of a file
* @param updatedMetadata * @param updatedMetadata
*/ */
public async getMetaData() { public async getMetaData() {
if (this.name.endsWith('.metadata')) {
throw new Error('metadata files cannot have metadata');
}
const metadata = await MetaData.createForFile({ const metadata = await MetaData.createForFile({
file: this, file: this,
}); });
return metadata; return metadata;
} }
/**
* gets the contents as json
*/
public async getJsonData() {
const json = await this.getContentsAsString();
const parsed = await JSON.parse(json);
return parsed;
}
public async writeJsonData(dataArg: any) {
await this.updateWithContents({
contents: JSON.stringify(dataArg),
});
}
} }

View File

@ -4,9 +4,7 @@ import { File } from './classes.file.js';
export class MetaData { export class MetaData {
// static // static
public static async createForFile(optionsArg: { public static async createForFile(optionsArg: { file: File }) {
file: File;
}) {
const metaData = new MetaData(); const metaData = new MetaData();
metaData.fileRef = optionsArg.file; metaData.fileRef = optionsArg.file;
@ -34,10 +32,10 @@ export class MetaData {
useFileExtension?: boolean; useFileExtension?: boolean;
useMagicBytes?: boolean; useMagicBytes?: boolean;
}): Promise<string> { }): Promise<string> {
if (optionsArg && optionsArg.useFileExtension || optionsArg.useFileExtension === undefined) { if ((optionsArg && optionsArg.useFileExtension) || optionsArg.useFileExtension === undefined) {
return plugins.path.extname(this.fileRef.name); return plugins.path.extname(this.fileRef.name);
} }
}; }
/** /**
* gets the size of the fileRef * gets the size of the fileRef
@ -47,59 +45,56 @@ export class MetaData {
path: this.fileRef.getBasePath(), path: this.fileRef.getBasePath(),
}); });
return stat.size; return stat.size;
}; }
private prefixCustomMetaData = 'custom_'; private prefixCustomMetaData = 'custom_';
public async storeCustomMetaData<T = any>(optionsArg: { public async storeCustomMetaData<T = any>(optionsArg: { key: string; value: T }) {
key: string; const data = await this.metadataFile.getContentsAsString();
value: T; data[this.prefixCustomMetaData + optionsArg.key] = optionsArg.value;
}) { await this.metadataFile.writeJsonData(data);
const json = await this.metadataFile.getContentsAsString();
const parsed = await JSON.parse(json);
parsed[this.prefixCustomMetaData + optionsArg.key] = optionsArg.value;
await this.metadataFile.updateWithContents({
contents: JSON.stringify(parsed),
});
} }
public async getCustomMetaData<T = any>(optionsArg: { public async getCustomMetaData<T = any>(optionsArg: { key: string }): Promise<T> {
key: string; const data = await this.metadataFile.getJsonData();
}): Promise<T> { return data[this.prefixCustomMetaData + optionsArg.key];
const json = await this.metadataFile.getContentsAsString();
const parsed = await JSON.parse(json);
return parsed[this.prefixCustomMetaData + optionsArg.key];
} }
public async deleteCustomMetaData(optionsArg: { public async deleteCustomMetaData(optionsArg: { key: string }) {
key: string; const data = await this.metadataFile.getJsonData();
}) { delete data[this.prefixCustomMetaData + optionsArg.key];
const json = await this.metadataFile.getContentsAsString(); await this.metadataFile.writeJsonData(data);
const parsed = await JSON.parse(json);
delete parsed[this.prefixCustomMetaData + optionsArg.key];
await this.metadataFile.updateWithContents({
contents: JSON.stringify(parsed),
});
} }
/** /**
* set a lock on the ref file * set a lock on the ref file
* @param optionsArg * @param optionsArg
*/ */
public async setLock(optionsArg: { public async setLock(optionsArg: { lock: string; expires: number }) {
lock: string; const data = await this.metadataFile.getJsonData();
expires: Date; data.lock = optionsArg.lock;
}) { data.lockExpires = optionsArg.expires;
await this.metadataFile.writeJsonData(data);
} }
/** /**
* remove the lock on the ref file * remove the lock on the ref file
* @param optionsArg * @param optionsArg
*/ */
public async removeLock(optionsArg: { public async removeLock(optionsArg: { force: boolean }) {
force: boolean; const data = await this.metadataFile.getJsonData();
}) { delete data.lock;
delete data.lockExpires;
await this.metadataFile.writeJsonData(data);
}
public async checkLocked(): Promise<boolean> {
const data = await this.metadataFile.getJsonData();
return data.lock && data.lockExpires > Date.now();
}
public async getLockInfo(): Promise<{ lock: string; expires: number }> {
const data = await this.metadataFile.getJsonData();
return { lock: data.lock, expires: data.lockExpires };
} }
} }

22
ts/helpers.ts Normal file
View File

@ -0,0 +1,22 @@
import * as plugins from './plugins.js';
import * as interfaces from './interfaces.js';
export const reducePathDescriptorToPath = async (pathDescriptorArg: interfaces.IPathDecriptor): Promise<string> => {
let returnPath = ``
if (pathDescriptorArg.directory) {
if (pathDescriptorArg.path && plugins.path.isAbsolute(pathDescriptorArg.path)) {
console.warn('Directory is being ignored when path is absolute.');
returnPath = pathDescriptorArg.path;
} else if (pathDescriptorArg.path) {
returnPath = plugins.path.join(pathDescriptorArg.directory.getBasePath(), pathDescriptorArg.path);
}
} else if (pathDescriptorArg.path) {
returnPath = pathDescriptorArg.path;
} else {
throw new Error('You must specify either a path or a directory.');
}
if (returnPath.startsWith('/')) {
returnPath = returnPath.substring(1);
}
return returnPath;
}

6
ts/interfaces.ts Normal file
View File

@ -0,0 +1,6 @@
import type { Directory } from "./classes.directory.js";
export interface IPathDecriptor {
path?: string;
directory?: Directory;
}