Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa4c44ae04 | |||
708b0b63b1 | |||
8554554642 | |||
a04aabf78b | |||
47cf2cc2cb | |||
ef20e15d20 | |||
39a4bd6ab7 | |||
c2a30654c5 | |||
8085033de4 | |||
75dd1d43a9 | |||
8ba7cdc873 | |||
ed8db4536b | |||
96e3eadb31 | |||
e9426b9cc9 | |||
9801e15c32 | |||
cbfdd8e123 | |||
138c38ee30 | |||
a1e449cf94 | |||
aa9a2e9220 | |||
154854dc21 | |||
8e9041fbbf | |||
16a82ac50a | |||
0b396f19cf | |||
6ab77ece6e | |||
b7a1f2087c | |||
b0d41fa9a0 | |||
34082c38a7 | |||
8d160cefb0 | |||
cec9c07b7c | |||
383a5204f4 | |||
c7f0c97341 | |||
e7f60465ff |
140
.gitlab-ci.yml
140
.gitlab-ci.yml
@@ -1,140 +0,0 @@
|
|||||||
# gitzone ci_default
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .npmci_cache/
|
|
||||||
key: '$CI_BUILD_STAGE'
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- security
|
|
||||||
- test
|
|
||||||
- release
|
|
||||||
- metadata
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- npm install -g @shipzone/npmci
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# security stage
|
|
||||||
# ====================
|
|
||||||
mirror:
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci git mirror
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
auditProductionDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci command npm install --production --ignore-scripts
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command npm audit --audit-level=high --only=prod --production
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
auditDevDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci command npm install --ignore-scripts
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command npm audit --audit-level=high --only=dev
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# test stage
|
|
||||||
# ====================
|
|
||||||
|
|
||||||
testStable:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci npm test
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
testBuild:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command npm run build
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm publish
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# metadata stage
|
|
||||||
# ====================
|
|
||||||
codequality:
|
|
||||||
stage: metadata
|
|
||||||
allow_failure: true
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
script:
|
|
||||||
- npmci command npm install -g typescript
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- priv
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci trigger
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
pages:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci node install lts
|
|
||||||
- npmci command npm install -g @git.zone/tsdoc
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command tsdoc
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1 week
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
allow_failure: true
|
|
95
changelog.md
95
changelog.md
@@ -1,5 +1,100 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-15 - 3.3.9 - fix(docs)
|
||||||
|
Revise README with detailed usage examples and add local Claude settings
|
||||||
|
|
||||||
|
- Revamped README: reorganized content, added emojis and clearer headings for install, getting started, bucket/file/directory operations, streaming, metadata, trash/recovery, locking, and advanced configuration.
|
||||||
|
- Added many concrete code examples for SmartBucket, Bucket, Directory, File, streaming (node/web), RxJS replay subjects, metadata handling, trash workflow, file locking, magic-bytes detection, JSON operations, and cleaning bucket contents.
|
||||||
|
- Included testing instructions (pnpm test) and a Best Practices section with recommendations for strict mode, streaming, metadata, trash usage, and locking.
|
||||||
|
- Added .claude/settings.local.json to include local Claude configuration and tool permissions.
|
||||||
|
- No source code or public API changes; documentation and local tooling config only.
|
||||||
|
|
||||||
|
## 2025-08-15 - 3.3.8 - fix(tests)
|
||||||
|
Update tests to use @git.zone/tstest, upgrade dependencies, remove GitLab CI and add local CI/workspace config
|
||||||
|
|
||||||
|
- Tests: replace imports from @push.rocks/tapbundle with @git.zone/tstest/tapbundle and switch tap.start() to export default tap.start()
|
||||||
|
- Dependencies: bump @aws-sdk/client-s3 and several @push.rocks packages; upgrade @tsclass/tsclass to a newer major
|
||||||
|
- DevDependencies: upgrade @git.zone/tsbuild, @git.zone/tstest, @push.rocks/qenv, and @push.rocks/tapbundle
|
||||||
|
- CI/config: remove .gitlab-ci.yml, add .claude/settings.local.json
|
||||||
|
- Workspace: add pnpm-workspace.yaml and packageManager field in package.json
|
||||||
|
|
||||||
|
## 2024-12-02 - 3.3.7 - fix(package)
|
||||||
|
Update author field in package.json
|
||||||
|
|
||||||
|
- Corrected the author field from 'Lossless GmbH' to 'Task Venture Capital GmbH' in the package.json file.
|
||||||
|
|
||||||
|
## 2024-12-02 - 3.3.6 - fix(package)
|
||||||
|
Fix license field in package.json to reflect MIT licensing
|
||||||
|
|
||||||
|
|
||||||
|
## 2024-11-25 - 3.3.5 - fix(test)
|
||||||
|
Refactor trash test to improve metadata validation
|
||||||
|
|
||||||
|
- Added new checks in trash tests to ensure metadata files are correctly moved to trash.
|
||||||
|
- Validated the presence and integrity of metadata within trashed files.
|
||||||
|
|
||||||
|
## 2024-11-25 - 3.3.4 - fix(core)
|
||||||
|
Minor refactoring and cleanup of TypeScript source files for improved readability and maintainability.
|
||||||
|
|
||||||
|
|
||||||
|
## 2024-11-24 - 3.3.3 - fix(documentation)
|
||||||
|
Improved documentation accuracy and consistency
|
||||||
|
|
||||||
|
- Updated the project description to reflect the cloud-agnostic nature and advanced capabilities
|
||||||
|
- Enhanced the README with detailed explanations and code examples for advanced features like trash management
|
||||||
|
- Clarified the handling and importance of metadata using the MetaData utility
|
||||||
|
|
||||||
|
## 2024-11-24 - 3.3.2 - fix(documentation)
|
||||||
|
Updated keywords and description for clarity and consistency.
|
||||||
|
|
||||||
|
- Modified keywords and description in package.json and npmextra.json.
|
||||||
|
- Enhanced readme.md file structure and examples
|
||||||
|
|
||||||
|
## 2024-11-24 - 3.3.1 - fix(File)
|
||||||
|
Fixed issue with file restore metadata operations.
|
||||||
|
|
||||||
|
- Corrected the order of operations in the file restore function to ensure custom metadata is appropriately deleted after moving the file.
|
||||||
|
|
||||||
|
## 2024-11-24 - 3.3.0 - feat(core)
|
||||||
|
Enhanced directory handling and file restoration from trash
|
||||||
|
|
||||||
|
- Refined getSubDirectoryByName to handle file paths treated as directories.
|
||||||
|
- Introduced file restoration function from trash to original or specified paths.
|
||||||
|
|
||||||
|
## 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)
|
## 2024-10-16 - 3.0.23 - fix(dependencies)
|
||||||
Update package dependencies for improved functionality and security.
|
Update package dependencies for improved functionality and security.
|
||||||
|
|
||||||
|
19
license
Normal file
19
license
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2014 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@@ -8,28 +8,30 @@
|
|||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "push.rocks",
|
"gitscope": "push.rocks",
|
||||||
"gitrepo": "smartbucket",
|
"gitrepo": "smartbucket",
|
||||||
"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 providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.",
|
||||||
"npmPackagename": "@push.rocks/smartbucket",
|
"npmPackagename": "@push.rocks/smartbucket",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"cloud storage",
|
"cloud agnostic",
|
||||||
"object storage",
|
"object storage",
|
||||||
"bucket creation",
|
"bucket management",
|
||||||
"file management",
|
"file operations",
|
||||||
"directory management",
|
"directory management",
|
||||||
"data streaming",
|
"data streaming",
|
||||||
"multi-cloud",
|
|
||||||
"API",
|
|
||||||
"unified storage",
|
|
||||||
"S3",
|
"S3",
|
||||||
"minio",
|
"multi-cloud",
|
||||||
"file locking",
|
"file locking",
|
||||||
"metadata",
|
"metadata management",
|
||||||
"buffer handling",
|
"buffer handling",
|
||||||
"access key",
|
"access control",
|
||||||
"secret key",
|
"environment configuration",
|
||||||
"cloud agnostic"
|
"unified storage",
|
||||||
|
"bucket policies",
|
||||||
|
"trash management",
|
||||||
|
"file transfer",
|
||||||
|
"data management",
|
||||||
|
"streaming"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartbucket",
|
"name": "@push.rocks/smartbucket",
|
||||||
"version": "3.0.23",
|
"version": "3.3.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@push.rocks/smartbucket",
|
"name": "@push.rocks/smartbucket",
|
||||||
"version": "3.0.23",
|
"version": "3.3.9",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartpath": "^5.0.18",
|
||||||
|
57
package.json
57
package.json
@@ -1,33 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartbucket",
|
"name": "@push.rocks/smartbucket",
|
||||||
"version": "3.0.23",
|
"version": "3.3.9",
|
||||||
"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 providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.",
|
||||||
"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",
|
||||||
"author": "Lossless GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "UNLICENSED",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)"
|
"build": "(tsbuild --web --allowimplicitany)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.84",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsrun": "^1.2.49",
|
"@git.zone/tsrun": "^1.2.49",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tstest": "^2.3.2",
|
||||||
"@push.rocks/qenv": "^6.0.5",
|
"@push.rocks/qenv": "^6.1.2",
|
||||||
"@push.rocks/tapbundle": "^5.3.0"
|
"@push.rocks/tapbundle": "^6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.670.0",
|
"@aws-sdk/client-s3": "^3.864.0",
|
||||||
"@push.rocks/smartmime": "^2.0.2",
|
"@push.rocks/smartmime": "^2.0.4",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.4",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrx": "^3.0.7",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstream": "^3.2.4",
|
"@push.rocks/smartstream": "^3.2.5",
|
||||||
"@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.1.2"
|
"@tsclass/tsclass": "^9.2.0"
|
||||||
},
|
},
|
||||||
"private": false,
|
"private": false,
|
||||||
"files": [
|
"files": [
|
||||||
@@ -47,27 +47,30 @@
|
|||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"cloud storage",
|
"cloud agnostic",
|
||||||
"object storage",
|
"object storage",
|
||||||
"bucket creation",
|
"bucket management",
|
||||||
"file management",
|
"file operations",
|
||||||
"directory management",
|
"directory management",
|
||||||
"data streaming",
|
"data streaming",
|
||||||
"multi-cloud",
|
|
||||||
"API",
|
|
||||||
"unified storage",
|
|
||||||
"S3",
|
"S3",
|
||||||
"minio",
|
"multi-cloud",
|
||||||
"file locking",
|
"file locking",
|
||||||
"metadata",
|
"metadata management",
|
||||||
"buffer handling",
|
"buffer handling",
|
||||||
"access key",
|
"access control",
|
||||||
"secret key",
|
"environment configuration",
|
||||||
"cloud agnostic"
|
"unified storage",
|
||||||
|
"bucket policies",
|
||||||
|
"trash management",
|
||||||
|
"file transfer",
|
||||||
|
"data management",
|
||||||
|
"streaming"
|
||||||
],
|
],
|
||||||
"homepage": "https://code.foss.global/push.rocks/smartbucket",
|
"homepage": "https://code.foss.global/push.rocks/smartbucket",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://code.foss.global/push.rocks/smartbucket.git"
|
"url": "https://code.foss.global/push.rocks/smartbucket.git"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
|
||||||
}
|
}
|
||||||
|
8553
pnpm-lock.yaml
generated
8553
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- mongodb-memory-server
|
||||||
|
- puppeteer
|
@@ -1 +1,3 @@
|
|||||||
|
* The project uses the official s3 client, not the minio client.
|
||||||
|
* notice the difference between *Strict methods and the normal methods.
|
||||||
|
* metadata is handled though the MetaData class. Important!
|
||||||
|
654
readme.md
654
readme.md
@@ -1,312 +1,456 @@
|
|||||||
# @push.rocks/smartbucket
|
# @push.rocks/smartbucket 🪣
|
||||||
|
|
||||||
A TypeScript library for cloud-independent object storage, providing features like bucket creation, file and directory management, and data streaming.
|
> A powerful, cloud-agnostic TypeScript library for object storage with advanced features like file locking, metadata management, and intelligent trash handling.
|
||||||
|
|
||||||
## Install
|
## Install 📦
|
||||||
|
|
||||||
To install `@push.rocks/smartbucket`, you need to have Node.js and npm (Node Package Manager) installed. If they are installed, you can add `@push.rocks/smartbucket` to your project by running the following command in your project's root directory:
|
To install `@push.rocks/smartbucket`, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @push.rocks/smartbucket --save
|
npm install @push.rocks/smartbucket --save
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will download and install `@push.rocks/smartbucket` along with its required dependencies into your project's `node_modules` directory and save it as a dependency in your project's `package.json` file.
|
Or if you're using pnpm (recommended):
|
||||||
|
|
||||||
## Usage
|
```bash
|
||||||
|
pnpm add @push.rocks/smartbucket
|
||||||
|
```
|
||||||
|
|
||||||
`@push.rocks/smartbucket` is a TypeScript module designed to provide simple cloud-independent object storage functionality. It wraps various cloud storage providers such as AWS S3, Google Cloud Storage, and others, offering a unified API to manage storage buckets and objects within those buckets.
|
## Usage 🚀
|
||||||
|
|
||||||
In this guide, we will delve into the usage of SmartBucket, covering its full range of features from setting up the library to advanced usage scenarios.
|
### Introduction
|
||||||
|
|
||||||
|
`@push.rocks/smartbucket` provides a unified, cloud-agnostic API for object storage operations across major providers like AWS S3, Google Cloud Storage, MinIO, and more. It abstracts away provider-specific complexities while offering advanced features like metadata management, file locking, streaming operations, and intelligent trash management.
|
||||||
|
|
||||||
### Table of Contents
|
### Table of Contents
|
||||||
1. [Setting Up](#setting-up)
|
|
||||||
2. [Creating a New Bucket](#creating-a-new-bucket)
|
|
||||||
3. [Listing Buckets](#listing-buckets)
|
|
||||||
4. [Working with Files](#working-with-files)
|
|
||||||
- [Uploading Files](#uploading-files)
|
|
||||||
- [Downloading Files](#downloading-files)
|
|
||||||
- [Deleting Files](#deleting-files)
|
|
||||||
- [Streaming Files](#streaming-files)
|
|
||||||
5. [Working with Directories](#working-with-directories)
|
|
||||||
6. [Advanced Features](#advanced-features)
|
|
||||||
- [Bucket Policies](#bucket-policies)
|
|
||||||
- [Object Metadata](#object-metadata)
|
|
||||||
- [Cloud Agnostic](#cloud-agnostic)
|
|
||||||
|
|
||||||
### Setting Up
|
1. [🏁 Getting Started](#-getting-started)
|
||||||
|
2. [🗂️ Working with Buckets](#️-working-with-buckets)
|
||||||
|
3. [📁 File Operations](#-file-operations)
|
||||||
|
4. [📂 Directory Management](#-directory-management)
|
||||||
|
5. [🌊 Streaming Operations](#-streaming-operations)
|
||||||
|
6. [🔒 File Locking](#-file-locking)
|
||||||
|
7. [🏷️ Metadata Management](#️-metadata-management)
|
||||||
|
8. [🗑️ Trash & Recovery](#️-trash--recovery)
|
||||||
|
9. [⚡ Advanced Features](#-advanced-features)
|
||||||
|
10. [☁️ Cloud Provider Support](#️-cloud-provider-support)
|
||||||
|
|
||||||
First, ensure you are using ECMAScript modules (ESM) and TypeScript in your project for best compatibility. Here's how to import and initialize SmartBucket in a TypeScript file:
|
### 🏁 Getting Started
|
||||||
|
|
||||||
|
First, set up your storage connection:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import { SmartBucket } from '@push.rocks/smartbucket';
|
||||||
SmartBucket,
|
|
||||||
Bucket,
|
|
||||||
Directory,
|
|
||||||
File
|
|
||||||
} from '@push.rocks/smartbucket';
|
|
||||||
|
|
||||||
const mySmartBucket = new SmartBucket({
|
// Initialize with your cloud storage credentials
|
||||||
accessKey: "yourAccessKey",
|
const smartBucket = new SmartBucket({
|
||||||
accessSecret: "yourSecretKey",
|
accessKey: 'your-access-key',
|
||||||
endpoint: "yourEndpointURL",
|
accessSecret: 'your-secret-key',
|
||||||
port: 443, // Default is 443, can be customized for specific endpoint
|
endpoint: 's3.amazonaws.com', // Or your provider's endpoint
|
||||||
useSsl: true // Defaults to true
|
port: 443,
|
||||||
|
useSsl: true,
|
||||||
|
region: 'us-east-1' // Optional, defaults to 'us-east-1'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure to replace `"yourAccessKey"`, `"yourSecretKey"`, and `"yourEndpointURL"` with your actual credentials and endpoint URL. The `port` and `useSsl` options are optional and can be omitted if the defaults are acceptable.
|
### 🗂️ Working with Buckets
|
||||||
|
|
||||||
### Creating a New Bucket
|
#### Creating Buckets
|
||||||
|
|
||||||
To create a new bucket:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function createBucket(bucketName: string) {
|
// Create a new bucket
|
||||||
|
const myBucket = await smartBucket.createBucket('my-awesome-bucket');
|
||||||
|
console.log(`✅ Bucket created: ${myBucket.name}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Getting Existing Buckets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get a bucket reference
|
||||||
|
const existingBucket = await smartBucket.getBucketByName('existing-bucket');
|
||||||
|
|
||||||
|
// Or use strict mode (throws if bucket doesn't exist)
|
||||||
|
const bucketStrict = await smartBucket.getBucketByNameStrict('must-exist-bucket');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Removing Buckets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Delete a bucket (must be empty)
|
||||||
|
await smartBucket.removeBucket('old-bucket');
|
||||||
|
console.log('🗑️ Bucket removed');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 File Operations
|
||||||
|
|
||||||
|
#### Upload Files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bucket = await smartBucket.getBucketByName('my-bucket');
|
||||||
|
|
||||||
|
// Simple file upload
|
||||||
|
await bucket.fastPut({
|
||||||
|
path: 'documents/report.pdf',
|
||||||
|
contents: Buffer.from('Your file content here')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload with string content
|
||||||
|
await bucket.fastPut({
|
||||||
|
path: 'notes/todo.txt',
|
||||||
|
contents: 'Buy milk\nCall mom\nRule the world'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strict upload (returns File object)
|
||||||
|
const uploadedFile = await bucket.fastPutStrict({
|
||||||
|
path: 'images/logo.png',
|
||||||
|
contents: imageBuffer,
|
||||||
|
overwrite: true // Optional: control overwrite behavior
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Download Files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get file as Buffer
|
||||||
|
const fileContent = await bucket.fastGet({
|
||||||
|
path: 'documents/report.pdf'
|
||||||
|
});
|
||||||
|
console.log(`📄 File size: ${fileContent.length} bytes`);
|
||||||
|
|
||||||
|
// Get file as string
|
||||||
|
const textContent = fileContent.toString('utf-8');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Check File Existence
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const exists = await bucket.fastExists({
|
||||||
|
path: 'documents/report.pdf'
|
||||||
|
});
|
||||||
|
console.log(`File exists: ${exists ? '✅' : '❌'}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete Files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Permanent deletion
|
||||||
|
await bucket.fastRemove({
|
||||||
|
path: 'old-file.txt'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Copy & Move Files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Copy file within bucket
|
||||||
|
await bucket.fastCopy({
|
||||||
|
sourcePath: 'original/file.txt',
|
||||||
|
destinationPath: 'backup/file-copy.txt'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move file (copy + delete original)
|
||||||
|
await bucket.fastMove({
|
||||||
|
sourcePath: 'temp/draft.txt',
|
||||||
|
destinationPath: 'final/document.txt'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📂 Directory Management
|
||||||
|
|
||||||
|
SmartBucket provides powerful directory-like operations for organizing your files:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get base directory
|
||||||
|
const baseDir = await bucket.getBaseDirectory();
|
||||||
|
|
||||||
|
// List directories and files
|
||||||
|
const directories = await baseDir.listDirectories();
|
||||||
|
const files = await baseDir.listFiles();
|
||||||
|
|
||||||
|
console.log(`📁 Found ${directories.length} directories`);
|
||||||
|
console.log(`📄 Found ${files.length} files`);
|
||||||
|
|
||||||
|
// Navigate subdirectories
|
||||||
|
const subDir = await baseDir.getSubDirectoryByName('projects/2024');
|
||||||
|
|
||||||
|
// Create nested file
|
||||||
|
await subDir.fastPut({
|
||||||
|
path: 'report.pdf',
|
||||||
|
contents: reportBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get directory tree structure
|
||||||
|
const tree = await subDir.getTreeArray();
|
||||||
|
console.log('🌳 Directory tree:', tree);
|
||||||
|
|
||||||
|
// Create empty file as placeholder
|
||||||
|
await subDir.createEmptyFile('placeholder.txt');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌊 Streaming Operations
|
||||||
|
|
||||||
|
Handle large files efficiently with streaming:
|
||||||
|
|
||||||
|
#### Download Streams
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Node.js stream
|
||||||
|
const nodeStream = await bucket.fastGetStream(
|
||||||
|
{ path: 'large-video.mp4' },
|
||||||
|
'nodestream'
|
||||||
|
);
|
||||||
|
nodeStream.pipe(fs.createWriteStream('local-video.mp4'));
|
||||||
|
|
||||||
|
// Web stream (for modern environments)
|
||||||
|
const webStream = await bucket.fastGetStream(
|
||||||
|
{ path: 'large-file.zip' },
|
||||||
|
'webstream'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Upload Streams
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stream upload from file
|
||||||
|
const readStream = fs.createReadStream('big-data.csv');
|
||||||
|
await bucket.fastPutStream({
|
||||||
|
path: 'uploads/big-data.csv',
|
||||||
|
stream: readStream,
|
||||||
|
metadata: {
|
||||||
|
contentType: 'text/csv',
|
||||||
|
userMetadata: {
|
||||||
|
uploadedBy: 'data-team',
|
||||||
|
version: '2.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reactive Streams with RxJS
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get file as ReplaySubject for reactive programming
|
||||||
|
const replaySubject = await bucket.fastGetReplaySubject({
|
||||||
|
path: 'data/sensor-readings.json',
|
||||||
|
chunkSize: 1024
|
||||||
|
});
|
||||||
|
|
||||||
|
replaySubject.subscribe({
|
||||||
|
next: (chunk) => processChunk(chunk),
|
||||||
|
complete: () => console.log('✅ Stream complete')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 File Locking
|
||||||
|
|
||||||
|
Prevent accidental modifications with file locking:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const file = await bucket.getBaseDirectory()
|
||||||
|
.getFileStrict({ path: 'important-config.json' });
|
||||||
|
|
||||||
|
// Lock file for 10 minutes
|
||||||
|
await file.lock({ timeoutMillis: 600000 });
|
||||||
|
console.log('🔒 File locked');
|
||||||
|
|
||||||
|
// Try to modify locked file (will throw error)
|
||||||
try {
|
try {
|
||||||
const myBucket: Bucket = await mySmartBucket.createBucket(bucketName);
|
await file.delete();
|
||||||
console.log(`Bucket ${bucketName} created successfully.`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating bucket:", error);
|
console.log('❌ Cannot delete locked file');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the function
|
// Unlock when done
|
||||||
createBucket("exampleBucket");
|
await file.unlock();
|
||||||
|
console.log('🔓 File unlocked');
|
||||||
```
|
```
|
||||||
|
|
||||||
Bucket names must be unique across the storage service.
|
### 🏷️ Metadata Management
|
||||||
|
|
||||||
### Listing Buckets
|
Attach and manage metadata for your files:
|
||||||
|
|
||||||
Currently, SmartBucket does not include a direct method to list all buckets, but you can access the underlying client provided by the cloud storage SDK to perform such operations, depending on the SDK's capabilities.
|
|
||||||
|
|
||||||
### Working with Files
|
|
||||||
|
|
||||||
#### Uploading Files
|
|
||||||
|
|
||||||
To upload an object to a bucket:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function uploadFile(bucketName: string, filePath: string, fileContent: Buffer | string) {
|
const file = await bucket.getBaseDirectory()
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
.getFileStrict({ path: 'document.pdf' });
|
||||||
if (myBucket) {
|
|
||||||
await myBucket.fastPut({ path: filePath, contents: fileContent });
|
|
||||||
console.log(`File uploaded to ${bucketName} at ${filePath}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
// Get metadata handler
|
||||||
uploadFile("exampleBucket", "path/to/object.txt", "Hello, world!");
|
const metadata = await file.getMetaData();
|
||||||
```
|
|
||||||
|
|
||||||
#### Downloading Files
|
// Set custom metadata
|
||||||
|
await metadata.setCustomMetaData({
|
||||||
To download an object:
|
key: 'author',
|
||||||
|
value: 'John Doe'
|
||||||
```typescript
|
|
||||||
async function downloadFile(bucketName: string, filePath: string) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
const fileContent: Buffer = await myBucket.fastGet({ path: filePath });
|
|
||||||
console.log("Downloaded file content:", fileContent.toString());
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
downloadFile("exampleBucket", "path/to/object.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Deleting Files
|
|
||||||
|
|
||||||
To delete an object from a bucket:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function deleteFile(bucketName: string, filePath: string) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
await myBucket.fastRemove({ path: filePath });
|
|
||||||
console.log(`File at ${filePath} deleted from ${bucketName}.`);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
deleteFile("exampleBucket", "path/to/object.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Streaming Files
|
|
||||||
|
|
||||||
SmartBucket allows you to work with file streams, which can be useful for handling large files.
|
|
||||||
|
|
||||||
To read a file as a stream:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ReplaySubject } from '@push.rocks/smartrx';
|
|
||||||
|
|
||||||
async function readFileStream(bucketName: string, filePath: string) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
const fileStream: ReplaySubject<Buffer> = await myBucket.fastGetStream({ path: filePath });
|
|
||||||
fileStream.subscribe({
|
|
||||||
next(chunk: Buffer) {
|
|
||||||
console.log("Chunk received:", chunk.toString());
|
|
||||||
},
|
|
||||||
complete() {
|
|
||||||
console.log("File read completed.");
|
|
||||||
},
|
|
||||||
error(err) {
|
|
||||||
console.error("Error reading file stream:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
readFileStream("exampleBucket", "path/to/object.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
To write a file as a stream:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Readable } from 'stream';
|
|
||||||
|
|
||||||
async function writeFileStream(bucketName: string, filePath: string, readableStream: Readable) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
await myBucket.fastPutStream({ path: filePath, dataStream: readableStream });
|
|
||||||
console.log(`File streamed to ${bucketName} at ${filePath}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a readable stream from a string
|
|
||||||
const readable = new Readable();
|
|
||||||
readable.push('Hello world streamed as a file!');
|
|
||||||
readable.push(null); // End of stream
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
writeFileStream("exampleBucket", "path/to/streamedObject.txt", readable);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working with Directories
|
|
||||||
|
|
||||||
`@push.rocks/smartbucket` offers abstractions for directories within buckets for easier object management. You can create, list, and delete directories using the `Directory` class.
|
|
||||||
|
|
||||||
To list the contents of a directory:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function listDirectoryContents(bucketName: string, directoryPath: string) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
const baseDirectory: Directory = await myBucket.getBaseDirectory();
|
|
||||||
const targetDirectory: Directory = await baseDirectory.getSubDirectoryByName(directoryPath);
|
|
||||||
|
|
||||||
console.log('Listing directories:');
|
|
||||||
const directories = await targetDirectory.listDirectories();
|
|
||||||
directories.forEach(dir => {
|
|
||||||
console.log(`- ${dir.name}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Listing files:');
|
await metadata.setCustomMetaData({
|
||||||
const files = await targetDirectory.listFiles();
|
key: 'department',
|
||||||
files.forEach(file => {
|
value: 'Engineering'
|
||||||
console.log(`- ${file.name}`);
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
// Retrieve metadata
|
||||||
listDirectoryContents("exampleBucket", "some/directory/path");
|
const author = await metadata.getCustomMetaData({ key: 'author' });
|
||||||
|
console.log(`📝 Author: ${author}`);
|
||||||
|
|
||||||
|
// Get all metadata
|
||||||
|
const allMeta = await metadata.getAllCustomMetaData();
|
||||||
|
console.log('📋 All metadata:', allMeta);
|
||||||
```
|
```
|
||||||
|
|
||||||
To create a file within a directory:
|
### 🗑️ Trash & Recovery
|
||||||
|
|
||||||
|
SmartBucket includes an intelligent trash system for safe file deletion:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function createFileInDirectory(bucketName: string, directoryPath: string, fileName: string, fileContent: string) {
|
const file = await bucket.getBaseDirectory()
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
.getFileStrict({ path: 'important-data.xlsx' });
|
||||||
if (myBucket) {
|
|
||||||
const baseDirectory: Directory = await myBucket.getBaseDirectory();
|
|
||||||
const targetDirectory: Directory = await baseDirectory.getSubDirectoryByName(directoryPath);
|
|
||||||
await targetDirectory.createEmptyFile(fileName); // Create an empty file
|
|
||||||
const file = new File({ directoryRefArg: targetDirectory, fileName });
|
|
||||||
await file.updateWithContents({ contents: fileContent });
|
|
||||||
console.log(`File created: ${fileName}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
// Move to trash instead of permanent deletion
|
||||||
createFileInDirectory("exampleBucket", "some/directory", "newfile.txt", "Hello, world!");
|
await file.delete({ mode: 'trash' });
|
||||||
```
|
console.log('🗑️ File moved to trash');
|
||||||
|
|
||||||
### Advanced Features
|
// Access trash
|
||||||
|
const trash = await bucket.getTrash();
|
||||||
|
const trashDir = await trash.getTrashDir();
|
||||||
|
const trashedFiles = await trashDir.listFiles();
|
||||||
|
console.log(`📦 ${trashedFiles.length} files in trash`);
|
||||||
|
|
||||||
#### Bucket Policies
|
// Restore from trash
|
||||||
|
const trashedFile = await bucket.getBaseDirectory()
|
||||||
Manage bucket policies to control access permissions. This feature depends on the policies provided by the storage service (e.g., AWS S3, MinIO).
|
.getFileStrict({
|
||||||
|
path: 'important-data.xlsx',
|
||||||
#### Object Metadata
|
getFromTrash: true
|
||||||
|
|
||||||
Retrieve and modify object metadata. Metadata can be useful for storing additional information about an object.
|
|
||||||
|
|
||||||
To retrieve metadata:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function getObjectMetadata(bucketName: string, filePath: string) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
const metadata = await mySmartBucket.minioClient.statObject(bucketName, filePath);
|
|
||||||
console.log("Object metadata:", metadata);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
getObjectMetadata("exampleBucket", "path/to/object.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
To update metadata:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function updateObjectMetadata(bucketName: string, filePath: string, newMetadata: { [key: string]: string }) {
|
|
||||||
const myBucket: Bucket = await mySmartBucket.getBucketByName(bucketName);
|
|
||||||
if (myBucket) {
|
|
||||||
await myBucket.copyObject({
|
|
||||||
objectKey: filePath,
|
|
||||||
nativeMetadata: newMetadata,
|
|
||||||
deleteExistingNativeMetadata: false,
|
|
||||||
});
|
});
|
||||||
console.log(`Metadata updated for ${filePath}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Bucket ${bucketName} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the function
|
await trashedFile.restore({ useOriginalPath: true });
|
||||||
updateObjectMetadata("exampleBucket", "path/to/object.txt", {
|
console.log('♻️ File restored successfully');
|
||||||
customKey: "customValue"
|
|
||||||
|
// Permanent deletion from trash
|
||||||
|
await trash.emptyTrash();
|
||||||
|
console.log('🧹 Trash emptied');
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚡ Advanced Features
|
||||||
|
|
||||||
|
#### File Statistics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get detailed file statistics
|
||||||
|
const stats = await bucket.fastStat({ path: 'document.pdf' });
|
||||||
|
console.log(`📊 Size: ${stats.size} bytes`);
|
||||||
|
console.log(`📅 Last modified: ${stats.lastModified}`);
|
||||||
|
console.log(`🏷️ ETag: ${stats.etag}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Magic Bytes Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Read first bytes for file type detection
|
||||||
|
const magicBytes = await bucket.getMagicBytes({
|
||||||
|
path: 'mystery-file',
|
||||||
|
length: 16
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or from a File object
|
||||||
|
const file = await bucket.getBaseDirectory()
|
||||||
|
.getFileStrict({ path: 'image.jpg' });
|
||||||
|
const magic = await file.getMagicBytes({ length: 4 });
|
||||||
|
console.log(`🔮 Magic bytes: ${magic.toString('hex')}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON Data Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const file = await bucket.getBaseDirectory()
|
||||||
|
.getFileStrict({ path: 'config.json' });
|
||||||
|
|
||||||
|
// Read JSON data
|
||||||
|
const config = await file.getJsonData();
|
||||||
|
console.log('⚙️ Config loaded:', config);
|
||||||
|
|
||||||
|
// Update JSON data
|
||||||
|
config.version = '2.0';
|
||||||
|
config.updated = new Date().toISOString();
|
||||||
|
await file.writeJsonData(config);
|
||||||
|
console.log('💾 Config updated');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Directory & File Type Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Check if path is a directory
|
||||||
|
const isDir = await bucket.isDirectory({ path: 'uploads/' });
|
||||||
|
|
||||||
|
// Check if path is a file
|
||||||
|
const isFile = await bucket.isFile({ path: 'uploads/document.pdf' });
|
||||||
|
|
||||||
|
console.log(`Is directory: ${isDir ? '📁' : '❌'}`);
|
||||||
|
console.log(`Is file: ${isFile ? '📄' : '❌'}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Clean Bucket Contents
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Remove all files and directories (use with caution!)
|
||||||
|
await bucket.cleanAllContents();
|
||||||
|
console.log('🧹 Bucket cleaned');
|
||||||
|
```
|
||||||
|
|
||||||
|
### ☁️ Cloud Provider Support
|
||||||
|
|
||||||
|
SmartBucket works seamlessly with:
|
||||||
|
|
||||||
|
- ✅ **AWS S3** - Full compatibility with S3 API
|
||||||
|
- ✅ **Google Cloud Storage** - Via S3-compatible API
|
||||||
|
- ✅ **MinIO** - Self-hosted S3-compatible storage
|
||||||
|
- ✅ **DigitalOcean Spaces** - S3-compatible object storage
|
||||||
|
- ✅ **Backblaze B2** - Cost-effective cloud storage
|
||||||
|
- ✅ **Wasabi** - High-performance S3-compatible storage
|
||||||
|
- ✅ **Any S3-compatible provider**
|
||||||
|
|
||||||
|
The library automatically handles provider quirks and optimizes operations for each platform while maintaining a consistent API.
|
||||||
|
|
||||||
|
### 🔧 Advanced Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Configure with custom options
|
||||||
|
const smartBucket = new SmartBucket({
|
||||||
|
accessKey: process.env.S3_ACCESS_KEY,
|
||||||
|
accessSecret: process.env.S3_SECRET_KEY,
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
port: 443,
|
||||||
|
useSsl: true,
|
||||||
|
region: 'eu-central-1',
|
||||||
|
// Additional S3 client options can be passed through
|
||||||
|
});
|
||||||
|
|
||||||
|
// Environment-based configuration
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
const qenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
const smartBucket = new SmartBucket({
|
||||||
|
accessKey: await qenv.getEnvVarOnDemandStrict('S3_ACCESS_KEY'),
|
||||||
|
accessSecret: await qenv.getEnvVarOnDemandStrict('S3_SECRET'),
|
||||||
|
endpoint: await qenv.getEnvVarOnDemandStrict('S3_ENDPOINT'),
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cloud Agnostic
|
### 🧪 Testing
|
||||||
|
|
||||||
`@push.rocks/smartbucket` is designed to work with multiple cloud providers, allowing for easier migration or multi-cloud strategies. This means you can switch from one provider to another with minimal changes to your codebase.
|
SmartBucket is thoroughly tested. Run tests with:
|
||||||
|
|
||||||
Remember, each cloud provider has specific features and limitations. `@push.rocks/smartbucket` aims to abstract common functionalities, but always refer to the specific cloud provider's documentation for advanced features or limitations.
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
This guide covers the basic to advanced scenarios of using `@push.rocks/smartbucket`. For further details, refer to the API documentation and examples.
|
### 🤝 Best Practices
|
||||||
|
|
||||||
|
1. **Always use strict mode** for critical operations to catch errors early
|
||||||
|
2. **Implement proper error handling** for network and permission issues
|
||||||
|
3. **Use streaming** for large files to optimize memory usage
|
||||||
|
4. **Leverage metadata** for organizing and searching files
|
||||||
|
5. **Enable trash mode** for important data to prevent accidental loss
|
||||||
|
6. **Lock files** during critical operations to prevent race conditions
|
||||||
|
7. **Clean up resources** properly when done
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
0
test/helpers/prepare.ts
Normal file
0
test/helpers/prepare.ts
Normal file
7
test/test.metadata.ts
Normal file
7
test/test.metadata.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('test metadata functionality', async () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -1,4 +1,5 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { jestExpect } from '@push.rocks/tapbundle/node';
|
||||||
import { Qenv } from '@push.rocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
|
||||||
import * as smartbucket from '../ts/index.js';
|
import * as smartbucket from '../ts/index.js';
|
||||||
@@ -11,14 +12,81 @@ let baseDirectory: smartbucket.Directory;
|
|||||||
|
|
||||||
tap.test('should create a valid smartbucket', async () => {
|
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.getEnvVarOnDemandStrict('S3_ACCESSKEY'),
|
||||||
accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRET'),
|
accessSecret: await testQenv.getEnvVarOnDemandStrict('S3_ACCESSSECRET'),
|
||||||
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
endpoint: await testQenv.getEnvVarOnDemandStrict('S3_ENDPOINT'),
|
||||||
});
|
});
|
||||||
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
|
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
|
||||||
myBucket = await testSmartbucket.getBucketByName('testzone');
|
myBucket = await testSmartbucket.getBucketByNameStrict(await testQenv.getEnvVarOnDemandStrict('S3_BUCKET'),);
|
||||||
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
|
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
|
||||||
expect(myBucket.name).toEqual('testzone');
|
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.fastPutStrict({
|
||||||
|
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.fastPutStrict({
|
||||||
|
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' });
|
||||||
|
|
||||||
|
const getTrashContents = async () => {
|
||||||
|
const trash = await myBucket.getTrash();
|
||||||
|
const trashDir = await trash.getTrashDir();
|
||||||
|
return await trashDir.listFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
const trashedFiles = await getTrashContents();
|
||||||
|
expect(trashedFiles.length).toEqual(2);
|
||||||
|
|
||||||
|
const trashedMetaFile = trashedFiles.find(file => file.name.endsWith('.metadata'));
|
||||||
|
expect(trashedMetaFile).toBeDefined();
|
||||||
|
expect(trashedMetaFile).toBeInstanceOf(smartbucket.File);
|
||||||
|
|
||||||
|
jestExpect(await trashedMetaFile!.getJsonData()).toEqual({
|
||||||
|
custom_recycle: {
|
||||||
|
deletedAt: jestExpect.any(Number),
|
||||||
|
originalPath: "trashtest/trashme.txt",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should restore a file from trash', async () => {
|
||||||
|
const baseDirectory = await myBucket.getBaseDirectory();
|
||||||
|
const file = await baseDirectory.getFileStrict({
|
||||||
|
path: 'trashtest/trashme.txt',
|
||||||
|
getFromTrash: true
|
||||||
|
});
|
||||||
|
const trashFileMeta = await file.getMetaData();
|
||||||
|
const data = await trashFileMeta.getCustomMetaData({
|
||||||
|
key: 'recycle'
|
||||||
|
});
|
||||||
|
expect(file).toBeInstanceOf(smartbucket.File);
|
||||||
|
await file.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
30
test/test.ts
30
test/test.ts
@@ -1,4 +1,4 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { Qenv } from '@push.rocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
|
||||||
import * as smartbucket from '../ts/index.js';
|
import * as smartbucket from '../ts/index.js';
|
||||||
@@ -11,14 +11,20 @@ let baseDirectory: smartbucket.Directory;
|
|||||||
|
|
||||||
tap.test('should create a valid smartbucket', async () => {
|
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.getEnvVarOnDemandStrict('S3_ACCESSKEY'),
|
||||||
accessSecret: await testQenv.getEnvVarOnDemand('S3_SECRET'),
|
accessSecret: await testQenv.getEnvVarOnDemandStrict('S3_ACCESSSECRET'),
|
||||||
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
endpoint: await testQenv.getEnvVarOnDemandStrict('S3_ENDPOINT'),
|
||||||
});
|
});
|
||||||
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
|
expect(testSmartbucket).toBeInstanceOf(smartbucket.SmartBucket);
|
||||||
myBucket = await testSmartbucket.getBucketByName('testzone');
|
myBucket = await testSmartbucket.getBucketByNameStrict(await testQenv.getEnvVarOnDemandStrict('S3_BUCKET'),);
|
||||||
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
|
expect(myBucket).toBeInstanceOf(smartbucket.Bucket);
|
||||||
expect(myBucket.name).toEqual('testzone');
|
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 () => {
|
tap.skip.test('should create testbucket', async () => {
|
||||||
@@ -41,9 +47,12 @@ tap.test('should get data in bucket', async () => {
|
|||||||
const fileString = await myBucket.fastGet({
|
const fileString = await myBucket.fastGet({
|
||||||
path: 'hithere/socool.txt',
|
path: 'hithere/socool.txt',
|
||||||
});
|
});
|
||||||
const fileStringStream = await myBucket.fastGetStream({
|
const fileStringStream = await myBucket.fastGetStream(
|
||||||
|
{
|
||||||
path: 'hithere/socool.txt',
|
path: 'hithere/socool.txt',
|
||||||
}, 'nodestream');
|
},
|
||||||
|
'nodestream'
|
||||||
|
);
|
||||||
console.log(fileString);
|
console.log(fileString);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,8 +106,9 @@ tap.test('should get base directory', async () => {
|
|||||||
tap.test('should correctly build paths for sub directories', async () => {
|
tap.test('should correctly build paths for sub directories', async () => {
|
||||||
const dir4 = await baseDirectory.getSubDirectoryByName('dir3/dir4');
|
const dir4 = await baseDirectory.getSubDirectoryByName('dir3/dir4');
|
||||||
expect(dir4).toBeInstanceOf(smartbucket.Directory);
|
expect(dir4).toBeInstanceOf(smartbucket.Directory);
|
||||||
const dir4BasePath = dir4.getBasePath();
|
const dir4BasePath = dir4?.getBasePath();
|
||||||
console.log(dir4BasePath);
|
console.log(dir4BasePath);
|
||||||
|
expect(dir4BasePath).toEqual('dir3/dir4/');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('clean up directory style tests', async () => {
|
tap.test('clean up directory style tests', async () => {
|
||||||
@@ -116,4 +126,4 @@ tap.test('clean up directory style tests', async () => {
|
|||||||
await myBucket.fastRemove({ path: 'file1.txt' });
|
await myBucket.fastRemove({ path: 'file1.txt' });
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartbucket',
|
name: '@push.rocks/smartbucket',
|
||||||
version: '3.0.23',
|
version: '3.3.9',
|
||||||
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 providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.'
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ export class Bucket {
|
|||||||
public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) {
|
public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string) {
|
||||||
const command = new plugins.s3.ListBucketsCommand({});
|
const command = new plugins.s3.ListBucketsCommand({});
|
||||||
const buckets = await smartbucketRef.s3Client.send(command);
|
const buckets = await smartbucketRef.s3Client.send(command);
|
||||||
const foundBucket = buckets.Buckets.find((bucket) => bucket.Name === bucketNameArg);
|
const foundBucket = buckets.Buckets!.find((bucket) => bucket.Name === bucketNameArg);
|
||||||
|
|
||||||
if (foundBucket) {
|
if (foundBucket) {
|
||||||
console.log(`bucket with name ${bucketNameArg} exists.`);
|
console.log(`bucket with name ${bucketNameArg} exists.`);
|
||||||
@@ -52,7 +52,7 @@ export class Bucket {
|
|||||||
* gets the base directory of the bucket
|
* gets the base directory of the bucket
|
||||||
*/
|
*/
|
||||||
public async getBaseDirectory(): Promise<Directory> {
|
public async getBaseDirectory(): Promise<Directory> {
|
||||||
return new Directory(this, null, '');
|
return new Directory(this, null!, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +71,9 @@ export class Bucket {
|
|||||||
}
|
}
|
||||||
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
|
const checkPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
|
||||||
const baseDirectory = await this.getBaseDirectory();
|
const baseDirectory = await this.getBaseDirectory();
|
||||||
return await baseDirectory.getSubDirectoryByName(checkPath);
|
return await baseDirectory.getSubDirectoryByNameStrict(checkPath, {
|
||||||
|
getEmptyDirectory: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============
|
// ===============
|
||||||
@@ -86,14 +88,15 @@ export class Bucket {
|
|||||||
contents: string | Buffer;
|
contents: string | Buffer;
|
||||||
overwrite?: boolean;
|
overwrite?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<File> {
|
): Promise<File | null> {
|
||||||
try {
|
try {
|
||||||
const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
|
const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
|
||||||
const exists = await this.fastExists({ path: reducedPath });
|
const exists = await this.fastExists({ path: reducedPath });
|
||||||
|
|
||||||
if (exists && !optionsArg.overwrite) {
|
if (exists && !optionsArg.overwrite) {
|
||||||
console.error(`Object already exists at path '${reducedPath}' in bucket '${this.name}'.`);
|
const errorText = `Object already exists at path '${reducedPath}' in bucket '${this.name}'.`;
|
||||||
return;
|
console.error(errorText);
|
||||||
|
return null;
|
||||||
} else if (exists && optionsArg.overwrite) {
|
} else if (exists && optionsArg.overwrite) {
|
||||||
console.log(
|
console.log(
|
||||||
`Overwriting existing object at path '${reducedPath}' in bucket '${this.name}'.`
|
`Overwriting existing object at path '${reducedPath}' in bucket '${this.name}'.`
|
||||||
@@ -126,6 +129,14 @@ export class Bucket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* get file
|
||||||
*/
|
*/
|
||||||
@@ -150,7 +161,7 @@ export class Bucket {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await done.promise;
|
await done.promise;
|
||||||
return completeFile;
|
return completeFile!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,7 +229,7 @@ export class Bucket {
|
|||||||
return chunk;
|
return chunk;
|
||||||
},
|
},
|
||||||
finalFunction: async (cb) => {
|
finalFunction: async (cb) => {
|
||||||
return null;
|
return null!;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,6 +243,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,7 +342,9 @@ export class Bucket {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const destinationBucket = optionsArg.targetBucket || this;
|
const destinationBucket = optionsArg.targetBucket || this;
|
||||||
const exists = await destinationBucket.fastExists({ path: optionsArg.destinationPath });
|
const exists = await destinationBucket.fastExists({
|
||||||
|
path: optionsArg.destinationPath,
|
||||||
|
});
|
||||||
|
|
||||||
if (exists && !optionsArg.overwrite) {
|
if (exists && !optionsArg.overwrite) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -387,8 +401,8 @@ export class Bucket {
|
|||||||
await this.smartbucketRef.s3Client.send(command);
|
await this.smartbucketRef.s3Client.send(command);
|
||||||
console.log(`Object '${optionsArg.path}' exists in bucket '${this.name}'.`);
|
console.log(`Object '${optionsArg.path}' exists in bucket '${this.name}'.`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (error.name === 'NotFound') {
|
if (error?.name === 'NotFound') {
|
||||||
console.log(`Object '${optionsArg.path}' does not exist in bucket '${this.name}'.`);
|
console.log(`Object '${optionsArg.path}' does not exist in bucket '${this.name}'.`);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -423,8 +437,8 @@ export class Bucket {
|
|||||||
Prefix: checkPath,
|
Prefix: checkPath,
|
||||||
Delimiter: '/',
|
Delimiter: '/',
|
||||||
});
|
});
|
||||||
const response = await this.smartbucketRef.s3Client.send(command);
|
const { CommonPrefixes } = await this.smartbucketRef.s3Client.send(command);
|
||||||
return response.CommonPrefixes.length > 0;
|
return !!CommonPrefixes && CommonPrefixes.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isFile(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
|
public async isFile(pathDescriptor: interfaces.IPathDecriptor): Promise<boolean> {
|
||||||
@@ -434,7 +448,79 @@ export class Bucket {
|
|||||||
Prefix: checkPath,
|
Prefix: checkPath,
|
||||||
Delimiter: '/',
|
Delimiter: '/',
|
||||||
});
|
});
|
||||||
|
const { Contents } = await this.smartbucketRef.s3Client.send(command);
|
||||||
|
return !!Contents && 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 response = await this.smartbucketRef.s3Client.send(command);
|
||||||
return response.Contents.length > 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,9 +10,9 @@ export class Directory {
|
|||||||
public parentDirectoryRef: Directory;
|
public parentDirectoryRef: Directory;
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
public tree: string[];
|
public tree!: string[];
|
||||||
public files: string[];
|
public files!: string[];
|
||||||
public folders: string[];
|
public folders!: string[];
|
||||||
|
|
||||||
constructor(bucketRefArg: Bucket, parentDirectory: Directory, name: string) {
|
constructor(bucketRefArg: Bucket, parentDirectory: Directory, name: string) {
|
||||||
this.bucketRef = bucketRefArg;
|
this.bucketRef = bucketRefArg;
|
||||||
@@ -69,7 +69,7 @@ export class Directory {
|
|||||||
path: string;
|
path: string;
|
||||||
createWithContents?: string | Buffer;
|
createWithContents?: string | Buffer;
|
||||||
getFromTrash?: boolean;
|
getFromTrash?: boolean;
|
||||||
}): Promise<File> {
|
}): Promise<File | null> {
|
||||||
const pathDescriptor = {
|
const pathDescriptor = {
|
||||||
directory: this,
|
directory: this,
|
||||||
path: optionsArg.path,
|
path: optionsArg.path,
|
||||||
@@ -98,6 +98,19 @@ export class Directory {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* lists all files
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +123,7 @@ export class Directory {
|
|||||||
const response = await this.bucketRef.smartbucketRef.s3Client.send(command);
|
const response = await this.bucketRef.smartbucketRef.s3Client.send(command);
|
||||||
const fileArray: File[] = [];
|
const fileArray: File[] = [];
|
||||||
|
|
||||||
response.Contents.forEach((item) => {
|
response.Contents?.forEach((item) => {
|
||||||
if (item.Key && !item.Key.endsWith('/')) {
|
if (item.Key && !item.Key.endsWith('/')) {
|
||||||
const subtractedPath = item.Key.replace(this.getBasePath(), '');
|
const subtractedPath = item.Key.replace(this.getBasePath(), '');
|
||||||
if (!subtractedPath.includes('/')) {
|
if (!subtractedPath.includes('/')) {
|
||||||
@@ -178,23 +191,77 @@ export class Directory {
|
|||||||
/**
|
/**
|
||||||
* gets a sub directory by name
|
* gets a sub directory by name
|
||||||
*/
|
*/
|
||||||
public async getSubDirectoryByName(dirNameArg: string): Promise<Directory> {
|
public async getSubDirectoryByName(dirNameArg: string, optionsArg: {
|
||||||
const dirNameArray = dirNameArg.split('/');
|
/**
|
||||||
|
* in s3 a directory does not exist if it is empty
|
||||||
|
* this option returns a directory even if it is empty
|
||||||
|
*/
|
||||||
|
getEmptyDirectory?: boolean;
|
||||||
|
/**
|
||||||
|
* in s3 a directory does not exist if it is empty
|
||||||
|
* this option creates a directory even if it is empty using a initializer file
|
||||||
|
*/
|
||||||
|
createWithInitializerFile?: boolean;
|
||||||
|
/**
|
||||||
|
* if the path is a file path, it will be treated as a file and the parent directory will be returned
|
||||||
|
*/
|
||||||
|
couldBeFilePath?: boolean;
|
||||||
|
} = {}): Promise<Directory | null> {
|
||||||
|
|
||||||
const getDirectory = async (directoryArg: Directory, dirNameToSearch: string) => {
|
const dirNameArray = dirNameArg.split('/').filter(str => str.trim() !== "");
|
||||||
const directories = await directoryArg.listDirectories();
|
|
||||||
return directories.find((directory) => {
|
|
||||||
return directory.name === dirNameToSearch;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let wantedDirectory: Directory;
|
optionsArg = {
|
||||||
for (const dirNameToSearch of dirNameArray) {
|
getEmptyDirectory: false,
|
||||||
const directoryToSearchIn = wantedDirectory ? wantedDirectory : this;
|
createWithInitializerFile: false,
|
||||||
wantedDirectory = await getDirectory(directoryToSearchIn, dirNameToSearch);
|
...optionsArg,
|
||||||
}
|
}
|
||||||
|
|
||||||
return wantedDirectory;
|
|
||||||
|
const getDirectory = async (directoryArg: Directory, dirNameToSearch: string, isFinalDirectory: boolean) => {
|
||||||
|
const directories = await directoryArg.listDirectories();
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionsArg.couldBeFilePath) {
|
||||||
|
const baseDirectory = await this.bucketRef.getBaseDirectory();
|
||||||
|
const existingFile = await baseDirectory.getFile({
|
||||||
|
path: dirNameArg,
|
||||||
|
});
|
||||||
|
if (existingFile) {
|
||||||
|
const adjustedPath = dirNameArg.substring(0, dirNameArg.lastIndexOf('/'));
|
||||||
|
return this.getSubDirectoryByName(adjustedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wantedDirectory: Directory | null = null;
|
||||||
|
let counter = 0;
|
||||||
|
for (const dirNameToSearch of dirNameArray) {
|
||||||
|
counter++;
|
||||||
|
const directoryToSearchIn = wantedDirectory ? wantedDirectory : this;
|
||||||
|
wantedDirectory = await getDirectory(directoryToSearchIn, dirNameToSearch, counter === dirNameArray.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,7 +360,7 @@ export class Directory {
|
|||||||
*/
|
*/
|
||||||
mode?: 'permanent' | 'trash';
|
mode?: 'permanent' | 'trash';
|
||||||
}) {
|
}) {
|
||||||
const file = await this.getFile({
|
const file = await this.getFileStrict({
|
||||||
path: optionsArg.path,
|
path: optionsArg.path,
|
||||||
});
|
});
|
||||||
await file.delete({
|
await file.delete({
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -88,25 +92,24 @@ export class File {
|
|||||||
/**
|
/**
|
||||||
* deletes this file
|
* deletes this file
|
||||||
*/
|
*/
|
||||||
public async delete(optionsArg?: {
|
public async delete(optionsArg?: { mode: 'trash' | 'permanent' }) {
|
||||||
mode: 'trash' | 'permanent';
|
|
||||||
}) {
|
|
||||||
|
|
||||||
optionsArg = {
|
optionsArg = {
|
||||||
...{
|
...{
|
||||||
mode: 'permanent',
|
mode: 'permanent',
|
||||||
},
|
},
|
||||||
...optionsArg,
|
...optionsArg,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (optionsArg.mode === 'permanent') {
|
if (optionsArg.mode === 'permanent') {
|
||||||
await this.parentDirectoryRef.bucketRef.fastRemove({
|
await this.parentDirectoryRef.bucketRef.fastRemove({
|
||||||
path: this.getBasePath(),
|
path: this.getBasePath(),
|
||||||
});
|
});
|
||||||
if (!this.name.endsWith('.metadata')) {
|
if (!this.name.endsWith('.metadata')) {
|
||||||
|
if (await this.hasMetaData()) {
|
||||||
const metadata = await this.getMetaData();
|
const metadata = await this.getMetaData();
|
||||||
await metadata.metadataFile.delete(optionsArg);
|
await metadata.metadataFile.delete(optionsArg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (optionsArg.mode === 'trash') {
|
} else if (optionsArg.mode === 'trash') {
|
||||||
const metadata = await this.getMetaData();
|
const metadata = await this.getMetaData();
|
||||||
await metadata.storeCustomMetaData({
|
await metadata.storeCustomMetaData({
|
||||||
@@ -117,8 +120,9 @@ export class File {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const trash = await this.parentDirectoryRef.bucketRef.getTrash();
|
const trash = await this.parentDirectoryRef.bucketRef.getTrash();
|
||||||
|
const trashDir = await trash.getTrashDir();
|
||||||
await this.move({
|
await this.move({
|
||||||
directory: await trash.getTrashDir(),
|
directory: trashDir,
|
||||||
path: await trash.getTrashKeyByOriginalBasePath(this.getBasePath()),
|
path: await trash.getTrashKeyByOriginalBasePath(this.getBasePath()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,33 @@ export class File {
|
|||||||
await this.parentDirectoryRef.listFiles();
|
await this.parentDirectoryRef.listFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* restores
|
||||||
|
*/
|
||||||
|
public async restore(optionsArg: {
|
||||||
|
useOriginalPath?: boolean;
|
||||||
|
toPath?: string;
|
||||||
|
overwrite?: boolean;
|
||||||
|
} = {}) {
|
||||||
|
optionsArg = {
|
||||||
|
useOriginalPath: (() => {
|
||||||
|
return optionsArg.toPath ? false : true;
|
||||||
|
})(),
|
||||||
|
overwrite: false,
|
||||||
|
...optionsArg,
|
||||||
|
};
|
||||||
|
const metadata = await this.getMetaData();
|
||||||
|
const moveToPath = optionsArg.toPath || (await metadata.getCustomMetaData({
|
||||||
|
key: 'recycle'
|
||||||
|
})).originalPath;
|
||||||
|
await metadata.deleteCustomMetaData({
|
||||||
|
key: 'recycle'
|
||||||
|
})
|
||||||
|
await this.move({
|
||||||
|
path: moveToPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* allows locking the file
|
* allows locking the file
|
||||||
* @param optionsArg
|
* @param optionsArg
|
||||||
@@ -150,7 +181,7 @@ export class File {
|
|||||||
}) {
|
}) {
|
||||||
const metadata = await this.getMetaData();
|
const metadata = await this.getMetaData();
|
||||||
await metadata.removeLock({
|
await metadata.removeLock({
|
||||||
force: optionsArg?.force,
|
force: optionsArg?.force || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,16 +196,19 @@ export class File {
|
|||||||
await this.parentDirectoryRef.bucketRef.fastPutStream({
|
await this.parentDirectoryRef.bucketRef.fastPutStream({
|
||||||
path: this.getBasePath(),
|
path: this.getBasePath(),
|
||||||
readableStream: optionsArg.contents,
|
readableStream: optionsArg.contents,
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
} else if (Buffer.isBuffer(optionsArg.contents)) {
|
} else if (Buffer.isBuffer(optionsArg.contents)) {
|
||||||
await this.parentDirectoryRef.bucketRef.fastPut({
|
await this.parentDirectoryRef.bucketRef.fastPut({
|
||||||
path: this.getBasePath(),
|
path: this.getBasePath(),
|
||||||
contents: optionsArg.contents,
|
contents: optionsArg.contents,
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
} else if (typeof optionsArg.contents === 'string') {
|
} else if (typeof optionsArg.contents === 'string') {
|
||||||
await this.parentDirectoryRef.bucketRef.fastPut({
|
await this.parentDirectoryRef.bucketRef.fastPut({
|
||||||
path: this.getBasePath(),
|
path: this.getBasePath(),
|
||||||
contents: Buffer.from(optionsArg.contents, optionsArg.encoding),
|
contents: Buffer.from(optionsArg.contents, optionsArg.encoding),
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,23 +217,52 @@ export class File {
|
|||||||
* moves the file to another directory
|
* moves the file to another directory
|
||||||
*/
|
*/
|
||||||
public async move(pathDescriptorArg: interfaces.IPathDecriptor) {
|
public async move(pathDescriptorArg: interfaces.IPathDecriptor) {
|
||||||
let moveToPath = '';
|
let moveToPath: string = '';
|
||||||
const isDirectory = await this.parentDirectoryRef.bucketRef.isDirectory(pathDescriptorArg);
|
const isDirectory = await this.parentDirectoryRef.bucketRef.isDirectory(pathDescriptorArg);
|
||||||
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),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
moveToPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
|
||||||
}
|
}
|
||||||
// lets move the file
|
// lets move the file
|
||||||
await this.parentDirectoryRef.bucketRef.fastMove({
|
await this.parentDirectoryRef.bucketRef.fastMove({
|
||||||
sourcePath: this.getBasePath(),
|
sourcePath: this.getBasePath(),
|
||||||
destinationPath: moveToPath,
|
destinationPath: moveToPath,
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// lets move the metadatafile
|
// lets move the metadatafile
|
||||||
|
if (!this.name.endsWith('.metadata')) {
|
||||||
const metadata = await this.getMetaData();
|
const metadata = await this.getMetaData();
|
||||||
await metadata.metadataFile.move(pathDescriptorArg);
|
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(
|
||||||
|
await helpers.reducePathDescriptorToPath(pathDescriptorArg),
|
||||||
|
{
|
||||||
|
couldBeFilePath: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,4 +293,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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,21 @@ import * as plugins from './plugins.js';
|
|||||||
import { File } from './classes.file.js';
|
import { File } from './classes.file.js';
|
||||||
|
|
||||||
export class MetaData {
|
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
|
// static
|
||||||
public static async createForFile(optionsArg: { file: File }) {
|
public static async createForFile(optionsArg: { file: File }) {
|
||||||
const metaData = new MetaData();
|
const metaData = new MetaData();
|
||||||
metaData.fileRef = optionsArg.file;
|
metaData.fileRef = optionsArg.file;
|
||||||
|
|
||||||
// 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.getFileStrict({
|
||||||
path: metaData.fileRef.name + '.metadata',
|
path: metaData.fileRef.name + '.metadata',
|
||||||
createWithContents: '{}',
|
createWithContents: '{}',
|
||||||
});
|
});
|
||||||
@@ -21,20 +29,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 +66,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);
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,15 @@ export class SmartBucket {
|
|||||||
await Bucket.removeBucketByName(this, bucketName);
|
await Bucket.removeBucketByName(this, bucketName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBucketByName(bucketName: string) {
|
public async getBucketByName(bucketNameArg: string) {
|
||||||
return Bucket.getBucketByName(this, bucketName);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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({ path: trashKey });
|
return trashDir.getFileStrict({ path: trashKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTrashKeyByOriginalBasePath (originalPath: string): Promise<string> {
|
public async getTrashKeyByOriginalBasePath (originalPath: string): Promise<string> {
|
||||||
|
@@ -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"
|
||||||
|
Reference in New Issue
Block a user