Compare commits
59 Commits
954df5a0ee
...
v11.2.6
Author | SHA1 | Date | |
---|---|---|---|
0001b433bf | |||
cd147ca38e | |||
9b0d89b9ef | |||
b34ae1362d | |||
2fa68c23a9 | |||
4083a36d8c | |||
3988ccbcb3 | |||
6eadbc654f | |||
fda1543701 | |||
a9660eda9a | |||
dfd1db152b | |||
7a32835a74 | |||
e78682d9b4 | |||
8dceea67be | |||
40018532a7 | |||
f6fb28d32f | |||
2d1ac0bd50 | |||
04a25221a5 | |||
13081b7344 | |||
0abbe8bbd7 | |||
de2a250a45 | |||
1657d0e1c6 | |||
e6b8240031 | |||
be011a4637 | |||
dbddf2a8ba | |||
207320ff26 | |||
be99bdae66 | |||
768d970918 | |||
a9799e05ee | |||
7c07c5c174 | |||
4c4e41b158 | |||
d557e4b4fe | |||
16ded5c3cf | |||
a2bd049ebd | |||
f95c5c9a15 | |||
b55a3eb83f | |||
fc8d994943 | |||
f1daec6f44 | |||
6698a583e9 | |||
bf52f01365 | |||
14adec5ba3 | |||
fb2880c995 | |||
0e0ae7e42f | |||
1391dbddbc | |||
477736da82 | |||
26a67d9662 | |||
14771fab27 | |||
b0ae383622 | |||
921f1cbb66 | |||
a9b373f5f8 | |||
f0d48cc763 | |||
255cc844ad | |||
50cc3fa8bf | |||
21b3870a7d | |||
023e1cdf29 | |||
493a235065 | |||
e4e48dc9db | |||
7de647daa1 | |||
34bc92137b |
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Run npm prepare
|
- name: Run npm prepare
|
||||||
run: npmci npm prepare
|
run: npmci npm prepare
|
||||||
|
@@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,7 +3,6 @@
|
|||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
public/
|
public/
|
||||||
pages/
|
|
||||||
|
|
||||||
# installs
|
# installs
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -17,4 +16,8 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
# custom
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
#------# custom
|
470
changelog.md
Normal file
470
changelog.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-18 - 11.2.6 - fix(fs)
|
||||||
|
Improve fs and stream handling, enhance SmartFile/StreamFile, update tests and CI configs
|
||||||
|
|
||||||
|
- Fix listFileTree to correctly handle '**/' patterns by running both root and nested patterns and deduplicating results.
|
||||||
|
- Enhance waitForFileToBeReady: support directory paths (wait for first file) and improved file-stability checks with timeouts and retries.
|
||||||
|
- StreamFile improvements: support Web ReadableStream -> Node Readable conversion, better multi-use buffering, more robust fromUrl/fromBuffer/fromStream implementations, and accurate byte-length computation.
|
||||||
|
- SmartFile updates: switch fromUrl to SmartRequest, robust rename (optional on-disk rename), safer read/write paths and consistent Buffer handling for hashing and content edits.
|
||||||
|
- fs module tweaks: copy/copySync gained replaceTargetDir option, improved toObjectSync error messages, toReadStream now validates existence, and various synchronous/async API consistency fixes.
|
||||||
|
- memory module: consistent async/sync write APIs and smartfileArrayToFs formatting/behavior fixes.
|
||||||
|
- fsstream: improved stream processing and SmartReadStream robustness (error handling, read logic and watcher improvements).
|
||||||
|
- Tests: reformatted and strengthened tests (more explicit assertions, added tests for '**/*.ts' and '**/*' edge cases, updated imports to tapbundle usage).
|
||||||
|
- CI/workflows: updated IMAGE and NPMCI_COMPUTED_REPOURL values and switched npmci package install to @ship.zone/npmci in workflow files.
|
||||||
|
- package.json: bumped various dependencies, updated test script (timeout, logfile), added typings/main fields, homepage fix, pnpm overrides and minor metadata fixes.
|
||||||
|
- .gitignore: added entries for AI tool folders (.claude/, .serena/).
|
||||||
|
- Docs/readme: expanded README with clearer examples, features and TypeScript usage; updated readme hints for listFileTree behavior.
|
||||||
|
|
||||||
|
## 2025-05-26 - 11.2.5 - fix(dev)
|
||||||
|
|
||||||
|
Update dev dependencies and add local permission settings
|
||||||
|
|
||||||
|
- Bump @git.zone/tsbuild from 2.5.2 to 2.6.4
|
||||||
|
- Bump @git.zone/tstest from 1.9.0 to 2.2.5
|
||||||
|
- Add .claude/settings.local.json to configure permissions for Bash(pnpm test:\*)
|
||||||
|
|
||||||
|
## 2025-05-24 - 11.2.4 - fix(config)
|
||||||
|
|
||||||
|
Add local permissions configuration for pnpm test commands in .claude/settings.local.json
|
||||||
|
|
||||||
|
- Introduced .claude/settings.local.json to allow Bash(pnpm test:\*) permissions
|
||||||
|
- Ensured local testing commands have proper execution rights
|
||||||
|
|
||||||
|
## 2025-05-21 - 11.2.2 - fix(tests/settings)
|
||||||
|
|
||||||
|
Improve test assertions and update local settings permissions
|
||||||
|
|
||||||
|
- Refactor StreamFile tests to assert content string type using toBeTypeofString
|
||||||
|
- Update file existence tests to use resolves.toBeTrue and resolves.toBeFalse for cleaner promise handling
|
||||||
|
- Add .claude/settings.local.json to allow specific Bash permissions for pnpm test commands
|
||||||
|
|
||||||
|
## 2025-05-21 - 11.2.1 - fix(fs)
|
||||||
|
|
||||||
|
Fix inconsistent glob matching in listFileTree and update test imports and dependency versions for enhanced stability.
|
||||||
|
|
||||||
|
- Enhanced listFileTree to support \*\*/ patterns by using dual patterns (root and nested) with deduplication.
|
||||||
|
- Updated test imports to use '@git.zone/tstest/tapbundle' for consistency across test files.
|
||||||
|
- Bumped dependency versions (@push.rocks/lik, smartpromise, smartrequest, glob) in package.json.
|
||||||
|
- Added npm configuration (.npmrc) and local settings for improved test verbosity.
|
||||||
|
|
||||||
|
## 2025-01-29 - 11.2.0 - feat(fs)
|
||||||
|
|
||||||
|
Enhanced copy method with optional replaceTargetDir option for directory replacement
|
||||||
|
|
||||||
|
- Added optional 'replaceTargetDir' option to 'copy' and 'copySync' methods in 'fs.ts'.
|
||||||
|
- The 'replaceTargetDir' option allows replacing the target directory if both source and target are directories.
|
||||||
|
|
||||||
|
## 2025-01-29 - 11.1.9 - fix(fs)
|
||||||
|
|
||||||
|
Fix directory handling in copy and copySync functions
|
||||||
|
|
||||||
|
- Ensured existing directories at destination are removed before copying over them in async copy.
|
||||||
|
- Added a similar check and handling for synchronous copySync when destination is a directory.
|
||||||
|
|
||||||
|
## 2025-01-29 - 11.1.8 - fix(fs)
|
||||||
|
|
||||||
|
Fixed copy and copySync functions to ensure they always overwrite files.
|
||||||
|
|
||||||
|
- Fixed bug in copy function where files were not being overwritten when they already existed at the destination.
|
||||||
|
- Fixed bug in copySync function to ensure files are overwritten to match the async function's behavior.
|
||||||
|
|
||||||
|
## 2025-01-29 - 11.1.7 - fix(fs)
|
||||||
|
|
||||||
|
Refactor copy and copySync functions to simplify return type
|
||||||
|
|
||||||
|
- Changed the return type of fs.copy and fs.copySync from boolean to void.
|
||||||
|
- Removed unnecessary promise handling in fs.copy.
|
||||||
|
|
||||||
|
## 2025-01-29 - 11.1.6 - fix(fs)
|
||||||
|
|
||||||
|
Fix issues with fs file copy functions.
|
||||||
|
|
||||||
|
- Updated dependencies in package.json.
|
||||||
|
- Corrected comments for asynchronous and synchronous file copy functions in fs.ts.
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.5 - fix(fs)
|
||||||
|
|
||||||
|
Improve waitForFileToBeReady function to handle directories and file stabilization
|
||||||
|
|
||||||
|
- Enhanced the waitForFileToBeReady to handle directory paths by checking for file existence within directories and waiting for stabilization.
|
||||||
|
- Modified the watcher logic to cater to changes when monitoring directories for file appearance.
|
||||||
|
- Introduced a helper function to ensure paths exist and another to resolve the first file in directories.
|
||||||
|
- Corrected logic for polling and stabilizing files within directories.
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.4 - fix(fs)
|
||||||
|
|
||||||
|
Fix file existence check in waitForFileToBeReady method.
|
||||||
|
|
||||||
|
- Ensured that the directory and file exist before setting up the watcher in waitForFileToBeReady.
|
||||||
|
- Changed ensureDirectoryExists to ensureFileExists for correct file path verification.
|
||||||
|
- Handled ENOENT errors correctly to retry file existence checks until timeout is reached.
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.3 - fix(fs)
|
||||||
|
|
||||||
|
Fix TypeScript type issue in fs module
|
||||||
|
|
||||||
|
- Corrected a TypeScript type in the fs module's checkFileStability function.
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.2 - fix(fs)
|
||||||
|
|
||||||
|
Fix issues in file stability check and directory existence verification in fs module
|
||||||
|
|
||||||
|
- Removed unused variable 'isFileAvailable' in 'waitForFileToBeReady'.
|
||||||
|
- Fixed logic for ensuring the target directory exists before setting up file stability watcher.
|
||||||
|
- Refactored directory existence logic into 'ensureDirectoryExists' function.
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.1 - fix(fs)
|
||||||
|
|
||||||
|
Improve waitForFileToBeReady function for file stability detection
|
||||||
|
|
||||||
|
- Enhanced error handling and file stability checks in waitForFileToBeReady function
|
||||||
|
- Added timeout feature for file readiness check
|
||||||
|
- Improved directory access check before file availability check
|
||||||
|
|
||||||
|
## 2025-01-07 - 11.1.0 - feat(SmartFile)
|
||||||
|
|
||||||
|
Add rename functionality to SmartFile class
|
||||||
|
|
||||||
|
- Implemented a new method to rename a file within the SmartFile class.
|
||||||
|
- The rename method updates the file path and optionally writes the renamed file to the disk.
|
||||||
|
|
||||||
|
## 2024-12-15 - 11.0.23 - fix(fs)
|
||||||
|
|
||||||
|
Handle errors in toObjectSync method
|
||||||
|
|
||||||
|
- Added error handling in toObjectSync function to capture and provide more informative error messages.
|
||||||
|
|
||||||
|
## 2024-06-23 - 11.0.22 - fix(core)
|
||||||
|
|
||||||
|
Update dependencies and changelog
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartstream to ^3.0.44
|
||||||
|
- Updated glob to ^10.4.2
|
||||||
|
- Updated @types/node to ^20.14.8
|
||||||
|
|
||||||
|
## 2024-06-23 - 11.0.21 - fix(dependencies)
|
||||||
|
|
||||||
|
Update dependencies to latest versions
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartpromise to ^4.0.4
|
||||||
|
- Updated @push.rocks/smartstream to ^3.0.44
|
||||||
|
- Updated glob to ^10.4.2
|
||||||
|
- Updated @types/node to ^20.14.8
|
||||||
|
|
||||||
|
## 2024-06-07 - 11.0.20 - Changelog
|
||||||
|
|
||||||
|
11.0.20
|
||||||
|
|
||||||
|
## 2024-06-07 - 11.0.19 - fix(core): update
|
||||||
|
|
||||||
|
11.0.19
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-06-07 - 11.0.18 - fix(core): update
|
||||||
|
|
||||||
|
11.0.18
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-06-06 - 11.0.17 - fix(core): update
|
||||||
|
|
||||||
|
11.0.17
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-06-06 - 11.0.16 - fix(core): update
|
||||||
|
|
||||||
|
11.0.16
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-05-29 - 11.0.16 - update description
|
||||||
|
|
||||||
|
11.0.16
|
||||||
|
|
||||||
|
- update description
|
||||||
|
|
||||||
|
## 2024-05-17 - 11.0.15 - fix(core): update
|
||||||
|
|
||||||
|
11.0.15
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-14 - 11.0.14 - update tsconfig
|
||||||
|
|
||||||
|
11.0.14
|
||||||
|
|
||||||
|
- update tsconfig
|
||||||
|
|
||||||
|
## 2024-04-12 - 11.0.13 - fix(core): update
|
||||||
|
|
||||||
|
11.0.13
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-12 - 11.0.12 - fix(core): update
|
||||||
|
|
||||||
|
11.0.12
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-12 - 11.0.11 - fix(core): update
|
||||||
|
|
||||||
|
11.0.11
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-03 - 11.0.10 - fix(core): update
|
||||||
|
|
||||||
|
11.0.10
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-03 - 11.0.9 - fix(core): update
|
||||||
|
|
||||||
|
11.0.9
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-02 - 11.0.8 - fix(core): update
|
||||||
|
|
||||||
|
11.0.8
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-02 - 11.0.7 - fix(core): update
|
||||||
|
|
||||||
|
11.0.7
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-02 - 11.0.6 - fix(core): update
|
||||||
|
|
||||||
|
11.0.6
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2024-04-01 - 11.0.5 - update npmextra.json
|
||||||
|
|
||||||
|
11.0.5
|
||||||
|
|
||||||
|
- update npmextra.json: githost
|
||||||
|
|
||||||
|
## 2024-04-01 - 11.0.4 - fix(core): update
|
||||||
|
|
||||||
|
11.0.4
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-24 - 11.0.3 - fix(core): update
|
||||||
|
|
||||||
|
11.0.3
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-07 - 11.0.2 - fix(core): update
|
||||||
|
|
||||||
|
11.0.2
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-07 - 11.0.1 - fix(core): update
|
||||||
|
|
||||||
|
11.0.1
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-06 - 11.0.0 - fix(core): update
|
||||||
|
|
||||||
|
11.0.0
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-06 - 10.0.40 - BREAKING CHANGE(core): update
|
||||||
|
|
||||||
|
10.0.40
|
||||||
|
|
||||||
|
- BREAKING CHANGE(core): update
|
||||||
|
|
||||||
|
## 2023-11-04 - 10.0.39 - fix(core): update
|
||||||
|
|
||||||
|
10.0.39
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-04 - 10.0.38 - fix(core): update
|
||||||
|
|
||||||
|
10.0.38
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-04 - 10.0.37 - fix(core): update
|
||||||
|
|
||||||
|
10.0.37
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-03 - 10.0.36 - fix(core): update
|
||||||
|
|
||||||
|
10.0.36
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-03 - 10.0.35 - fix(core): update
|
||||||
|
|
||||||
|
10.0.35
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-03 - 10.0.34 - fix(core): update
|
||||||
|
|
||||||
|
10.0.34
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-11-03 - 10.0.33 - fix(core): update
|
||||||
|
|
||||||
|
10.0.33
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-10-12 - 10.0.32 - fix(core): update
|
||||||
|
|
||||||
|
10.0.32
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-09-22 - 10.0.31 - fix(core): update
|
||||||
|
|
||||||
|
10.0.31
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-08-31 - 10.0.30 - fix(core): update
|
||||||
|
|
||||||
|
10.0.30
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-08-23 - 10.0.29 - fix(core): update
|
||||||
|
|
||||||
|
10.0.29
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-07-12 - 10.0.28 - fix(core): update
|
||||||
|
|
||||||
|
10.0.28
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-07-10 - 10.0.27 - fix(core): update
|
||||||
|
|
||||||
|
10.0.27
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-07-10 - 10.0.26 - fix(core): update
|
||||||
|
|
||||||
|
10.0.26
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-07-08 - 10.0.25 - fix(core): update
|
||||||
|
|
||||||
|
10.0.25
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2023-06-25 - 10.0.24 to 10.0.14 - Series of Fixes
|
||||||
|
|
||||||
|
10.0.24 to 10.0.14
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
|
||||||
|
## 2023-01-09 - 10.0.13 to 10.0.6 - Series of Fixes
|
||||||
|
|
||||||
|
10.0.13 to 10.0.6
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
|
||||||
|
## 2022-09-05 - 10.0.5 to 10.0.3 - Series of Fixes
|
||||||
|
|
||||||
|
10.0.5 to 10.0.3
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
|
||||||
|
## 2022-06-07 - 10.0.2 to 10.0.1 - Series of Fixes
|
||||||
|
|
||||||
|
10.0.2 to 10.0.1
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
|
||||||
|
## 2022-06-07 - 9.0.7 - BREAKING CHANGE(core): switch to esm
|
||||||
|
|
||||||
|
9.0.7
|
||||||
|
|
||||||
|
- BREAKING CHANGE(core): switch to esm
|
||||||
|
|
||||||
|
## 2022-03-11 - 9.0.6 to 9.0.2 - Series of Fixes
|
||||||
|
|
||||||
|
9.0.6 to 9.0.2
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
|
||||||
|
## 2021-12-01 - 9.0.1 - fix(core): update
|
||||||
|
|
||||||
|
9.0.1
|
||||||
|
|
||||||
|
- fix(core): update
|
||||||
|
|
||||||
|
## 2021-12-01 - 9.0.0 - fix(absolute pathing)
|
||||||
|
|
||||||
|
9.0.0
|
||||||
|
|
||||||
|
- add functions for easily getting absolute paths
|
||||||
|
|
||||||
|
## 2021-11-30 - 8.0.11 - BREAKING CHANGE(relative pathing)
|
||||||
|
|
||||||
|
8.0.11
|
||||||
|
|
||||||
|
- improved relative pathing
|
||||||
|
|
||||||
|
## 2020-08-10 - 8.0.10 to 7.0.12 - Series of Fixes and Updates
|
||||||
|
|
||||||
|
8.0.10 to 7.0.12
|
||||||
|
|
||||||
|
- Series of fixes in the core module
|
||||||
|
- BREAKING CHANGE(Smartfile class): switch to a Buffer-only approach
|
||||||
|
|
||||||
|
## 2019-02-17 - 7.0.0 - fix(core): update dependencies
|
||||||
|
|
||||||
|
7.0.0
|
||||||
|
|
||||||
|
- fix(core): update dependencies
|
||||||
|
|
||||||
|
## 2019-01-27 - 6.0.12 - BREAKING CHANGE(smartfile.fs.fileExists)
|
||||||
|
|
||||||
|
6.0.12
|
||||||
|
|
||||||
|
- now returns a Promise<boolean>
|
||||||
|
|
||||||
|
## 2018-08-19 - 6.0.11 to 6.0.6 - Series of Fixes
|
||||||
|
|
||||||
|
6.0.11 to 6.0.6
|
||||||
|
|
||||||
|
- Series of fixes in core and dependencies
|
||||||
|
|
||||||
|
## 2018-07-03 - 6.0.5 to 5.0.0 - Series of Fixes
|
||||||
|
|
||||||
|
6.0.5 to 5.0.0
|
||||||
|
|
||||||
|
- Series of fixes in core and dependencies
|
||||||
|
|
||||||
|
## 2018-02-16 - 4.2.28 - BREAKING CHANGE(scope)
|
||||||
|
|
||||||
|
4.2.28
|
||||||
|
|
||||||
|
- switch to pushrocks scope
|
@@ -10,30 +10,33 @@
|
|||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "push.rocks",
|
"gitscope": "push.rocks",
|
||||||
"gitrepo": "smartfile",
|
"gitrepo": "smartfile",
|
||||||
"description": "provides a robust suite of tools for managing files in Node.js using TypeScript.",
|
"description": "Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.",
|
||||||
"npmPackagename": "@push.rocks/smartfile",
|
"npmPackagename": "@push.rocks/smartfile",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"files management",
|
"file management",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"Node.js",
|
"Node.js",
|
||||||
"read files",
|
"file operations",
|
||||||
"write files",
|
"file manipulation",
|
||||||
"copy files",
|
|
||||||
"file streaming",
|
"file streaming",
|
||||||
"directories manipulation",
|
"virtual directory",
|
||||||
"virtual file system",
|
|
||||||
"filesystem utilities",
|
"filesystem utilities",
|
||||||
"ESM syntax",
|
"memory-efficient file handling",
|
||||||
"memory-efficient streaming"
|
"custom file operations",
|
||||||
|
"write files",
|
||||||
|
"read files",
|
||||||
|
"copy files",
|
||||||
|
"delete files",
|
||||||
|
"list directories",
|
||||||
|
"handle large files",
|
||||||
|
"virtual filesystems",
|
||||||
|
"buffer operations"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"tsdoc": {
|
||||||
"classes": [
|
"classes": ["SmartFile", "StreamFile"],
|
||||||
"SmartFile",
|
|
||||||
"StreamFile"
|
|
||||||
],
|
|
||||||
"descriptions": [
|
"descriptions": [
|
||||||
"the purpose of the StreamFile class is to provide a hybrid interface between streaming files and simple handling when writing and reading those files multiple times."
|
"the purpose of the StreamFile class is to provide a hybrid interface between streaming files and simple handling when writing and reading those files multiple times."
|
||||||
],
|
],
|
||||||
|
70
package.json
70
package.json
@@ -1,64 +1,68 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartfile",
|
"name": "@push.rocks/smartfile",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.0.11",
|
"version": "11.2.6",
|
||||||
"description": "provides a robust suite of tools for managing files in Node.js using TypeScript.",
|
"description": "Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.",
|
||||||
"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",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://gitlab.com/push.rocks/smartfile.git"
|
"url": "https://code.foss.global/push.rocks/smartfile.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"files management",
|
"file management",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"Node.js",
|
"Node.js",
|
||||||
"read files",
|
"file operations",
|
||||||
"write files",
|
"file manipulation",
|
||||||
"copy files",
|
|
||||||
"file streaming",
|
"file streaming",
|
||||||
"directories manipulation",
|
"virtual directory",
|
||||||
"virtual file system",
|
|
||||||
"filesystem utilities",
|
"filesystem utilities",
|
||||||
"ESM syntax",
|
"memory-efficient file handling",
|
||||||
"memory-efficient streaming"
|
"custom file operations",
|
||||||
|
"write files",
|
||||||
|
"read files",
|
||||||
|
"copy files",
|
||||||
|
"delete files",
|
||||||
|
"list directories",
|
||||||
|
"handle large files",
|
||||||
|
"virtual filesystems",
|
||||||
|
"buffer operations"
|
||||||
],
|
],
|
||||||
"author": "Lossless GmbH <hello@lossless.com> (https://lossless.com)",
|
"author": "Lossless GmbH <hello@lossless.com> (https://lossless.com)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://gitlab.com/push.rocks/smartfile/issues"
|
"url": "https://code.foss.global/push.rocks/smartfile/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/push.rocks/smartfile#readme",
|
"homepage": "https://code.foss.global/push.rocks/smartfile#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.0.12",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartfile-interfaces": "^1.0.7",
|
"@push.rocks/smartfile-interfaces": "^1.0.7",
|
||||||
"@push.rocks/smarthash": "^3.0.4",
|
"@push.rocks/smarthash": "^3.2.3",
|
||||||
"@push.rocks/smartjson": "^5.0.10",
|
"@push.rocks/smartjson": "^5.0.20",
|
||||||
"@push.rocks/smartmime": "^1.0.5",
|
"@push.rocks/smartmime": "^2.0.4",
|
||||||
"@push.rocks/smartpath": "^5.0.11",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.2",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^2.0.21",
|
"@push.rocks/smartrequest": "^4.2.1",
|
||||||
"@push.rocks/smartstream": "^3.0.30",
|
"@push.rocks/smartstream": "^3.2.5",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/glob": "^8.1.0",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.3.1",
|
||||||
"glob": "^10.3.10",
|
"glob": "^11.0.3",
|
||||||
"js-yaml": "^4.1.0"
|
"js-yaml": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.70",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^1.0.84",
|
"@git.zone/tstest": "^2.3.4",
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
"@types/node": "^22.15.21"
|
||||||
"@types/node": "^20.10.0"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -74,5 +78,9 @@
|
|||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
]
|
],
|
||||||
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13156
pnpm-lock.yaml
generated
13156
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
33
readme.hints.md
Normal file
33
readme.hints.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# SmartFile Implementation Hints
|
||||||
|
|
||||||
|
## listFileTree Function Enhancement (ts/fs.ts:367-415)
|
||||||
|
|
||||||
|
### Issue Fixed
|
||||||
|
|
||||||
|
The `listFileTree` function previously had inconsistent behavior with `**/*.extension` patterns across different systems and glob implementations. Some implementations would miss root-level files when using patterns like `**/*.ts`.
|
||||||
|
|
||||||
|
### Solution Implemented
|
||||||
|
|
||||||
|
Modified the function to explicitly handle `**/` patterns by:
|
||||||
|
|
||||||
|
1. Detecting when a pattern starts with `**/`
|
||||||
|
2. Extracting the file pattern after `**/` (e.g., `*.ts` from `**/*.ts`)
|
||||||
|
3. Running both the original pattern and the extracted root pattern
|
||||||
|
4. Using a Set to deduplicate results and ensure consistent ordering
|
||||||
|
|
||||||
|
### Key Benefits
|
||||||
|
|
||||||
|
- Guarantees consistent behavior across all systems
|
||||||
|
- Ensures both root-level and nested files are found with `**/*` patterns
|
||||||
|
- Maintains backward compatibility
|
||||||
|
- No performance degradation due to efficient deduplication
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
Added comprehensive tests to verify:
|
||||||
|
|
||||||
|
- Both root and nested files are found with `**/*.ts`
|
||||||
|
- No duplicate entries in results
|
||||||
|
- Edge cases with various file extensions work correctly
|
||||||
|
|
||||||
|
This fix ensures tools like `tsbuild check **/*.ts` work reliably across all systems.
|
439
readme.md
439
readme.md
@@ -1,263 +1,314 @@
|
|||||||
# @push.rocks/smartfile
|
# @push.rocks/smartfile 📁
|
||||||
|
|
||||||
> Provides a robust suite of tools for managing files in Node.js using TypeScript.
|
> **A powerful, TypeScript-based file management library for Node.js**
|
||||||
|
|
||||||
## Install
|
## 🚀 What is smartfile?
|
||||||
|
|
||||||
To integrate `@push.rocks/smartfile` into your project, run:
|
`@push.rocks/smartfile` is your go-to solution for file operations in Node.js. It offers a clean, promise-based API for handling files, directories, streams, and even virtual filesystems - all while maintaining maximum performance and reliability.
|
||||||
|
|
||||||
|
Think of it as `fs` on steroids, with TypeScript superpowers! 💪
|
||||||
|
|
||||||
|
## 💾 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @push.rocks/smartfile
|
npm install @push.rocks/smartfile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## ✨ Features
|
||||||
|
|
||||||
`@push.rocks/smartfile` offers a comprehensive set of utilities for handling files in Node.js projects using modern TypeScript and ESM syntax. It simplifies various file operations such as reading, writing, copying, and streaming files, as well as working with directories and virtual file systems.
|
- 🔥 **Streaming Support** - Handle massive files with ease using `StreamFile`
|
||||||
|
- 📦 **Virtual Directories** - Work with in-memory file structures
|
||||||
|
- 🌐 **URL Support** - Directly work with files from URLs
|
||||||
|
- 🎯 **TypeScript First** - Full type safety and IntelliSense support
|
||||||
|
- ⚡ **Promise-based API** - Modern async/await patterns throughout
|
||||||
|
- 🛠️ **Comprehensive Toolset** - From basic CRUD to advanced operations
|
||||||
|
|
||||||
### Key Features and Classes
|
## 📚 Quick Start
|
||||||
|
|
||||||
- **`SmartFile`**: Facilitates reading from and writing to individual files, managing metadata.
|
|
||||||
- **`StreamFile`**: Optimizes memory usage by enabling efficient file streaming.
|
|
||||||
- **`VirtualDirectory`**: Allows manipulation of a group of files or directories as a virtual file system.
|
|
||||||
|
|
||||||
### Getting Started with ESM and TypeScript
|
|
||||||
|
|
||||||
First, ensure your project supports ESM syntax and TypeScript. Then, begin by importing the desired features from `@push.rocks/smartfile`:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartFile, StreamFile, VirtualDirectory, memory, fs as smartFs } from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
|
||||||
|
// Read a file
|
||||||
|
const content = await smartfile.fs.toStringSync('./my-file.txt');
|
||||||
|
|
||||||
|
// Write a file
|
||||||
|
await smartfile.memory.toFs('Hello World!', './output.txt');
|
||||||
|
|
||||||
|
// Work with JSON
|
||||||
|
const data = await smartfile.fs.toObjectSync('./data.json');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reading and Writing Files
|
## 🎨 Core Components
|
||||||
|
|
||||||
#### Reading Files
|
### SmartFile Class
|
||||||
|
|
||||||
Reading a JSON file:
|
The `SmartFile` class represents a single file with powerful manipulation capabilities:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const myJsonFile: SmartFile = await SmartFile.fromFilePath('./data.json');
|
import { SmartFile } from '@push.rocks/smartfile';
|
||||||
const jsonData = JSON.parse(myJsonFile.contents.toString());
|
|
||||||
console.log(jsonData);
|
// Create from file path
|
||||||
|
const fileFromPath = await SmartFile.fromFilePath('./data.json');
|
||||||
|
|
||||||
|
// Create from URL
|
||||||
|
const fileFromUrl = await SmartFile.fromUrl('https://example.com/config.json');
|
||||||
|
|
||||||
|
// Create from text
|
||||||
|
const fileFromText = await SmartFile.fromString(
|
||||||
|
'./my-file.txt',
|
||||||
|
'This is my content',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create from Buffer
|
||||||
|
const fileFromBuffer = await SmartFile.fromBuffer(
|
||||||
|
'./binary.dat',
|
||||||
|
Buffer.from([0x00, 0x01, 0x02])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Edit content
|
||||||
|
await fileFromPath.editContentAsString(async (content) => {
|
||||||
|
return content.replace(/old/g, 'new');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write to disk
|
||||||
|
await fileFromPath.write();
|
||||||
|
|
||||||
|
// Get content
|
||||||
|
const contentString = fileFromPath.parseContentAsString();
|
||||||
|
const contentBuffer = fileFromPath.parseContentAsBuffer();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Writing Files
|
### StreamFile Class 🌊
|
||||||
|
|
||||||
Writing content to a file:
|
Perfect for handling large files without memory overhead:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const filePath: string = './output.txt';
|
import { StreamFile } from '@push.rocks/smartfile';
|
||||||
const content: string = 'Hello, SmartFile!';
|
|
||||||
await memory.toFs(content, filePath);
|
// Create from path
|
||||||
console.log('File saved successfully.');
|
const streamFile = await StreamFile.fromPath('./bigfile.zip');
|
||||||
|
|
||||||
|
// Create from URL
|
||||||
|
const urlStream = await StreamFile.fromUrl('https://example.com/large.mp4');
|
||||||
|
|
||||||
|
// Create from buffer
|
||||||
|
const bufferStream = StreamFile.fromBuffer(
|
||||||
|
Buffer.from('streaming content'),
|
||||||
|
'stream.txt'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write to disk
|
||||||
|
await streamFile.writeToDisk('./output/bigfile.zip');
|
||||||
|
|
||||||
|
// Get as buffer (careful with large files!)
|
||||||
|
const buffer = await streamFile.getContentAsBuffer();
|
||||||
|
|
||||||
|
// Get as stream
|
||||||
|
const readStream = await streamFile.createReadStream();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Streaming Large Files
|
### VirtualDirectory Class 📂
|
||||||
|
|
||||||
For large files, `StreamFile` provides a memory-efficient streaming solution:
|
Manage collections of files as virtual filesystems:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createReadStream } from 'fs';
|
import { VirtualDirectory } from '@push.rocks/smartfile';
|
||||||
const sourceStream = createReadStream('./large-video.mp4');
|
|
||||||
const myStreamFile = await StreamFile.fromStream(sourceStream, 'large-video.mp4');
|
// Create from filesystem
|
||||||
await myStreamFile.writeToDir('./storage');
|
const vDir = await VirtualDirectory.fromFsDirPath('./src');
|
||||||
console.log('Large file streamed to disk successfully.');
|
|
||||||
|
// Create from file array
|
||||||
|
const vDirFromFiles = await VirtualDirectory.fromFileArray([
|
||||||
|
await SmartFile.fromFilePath('./file1.txt'),
|
||||||
|
await SmartFile.fromFilePath('./file2.txt')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add files
|
||||||
|
vDir.addSmartfiles([
|
||||||
|
await SmartFile.fromString('./virtual/new.txt', 'content')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List files
|
||||||
|
const files = vDir.listFiles();
|
||||||
|
const directories = vDir.listDirectories();
|
||||||
|
|
||||||
|
// Get file
|
||||||
|
const file = vDir.getFileByPath('./some/path.txt');
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
await vDir.saveToDisk('./output');
|
||||||
|
|
||||||
|
// Load from disk
|
||||||
|
await vDir.loadFromDisk('./source');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Working with Virtual Directories
|
## 🛠️ File Operations
|
||||||
|
|
||||||
`VirtualDirectory` abstracts a collection of files allowing operations to be performed as if they were on disk:
|
### Basic Operations
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const virtualDir = new VirtualDirectory();
|
// Check existence
|
||||||
virtualDir.addSmartfiles([smartFile1, smartFile2]); // Assuming these are SmartFile instances
|
const exists = await smartfile.fs.fileExists('./file.txt');
|
||||||
await virtualDir.saveToDisk('./virtual-output');
|
const existsSync = smartfile.fs.fileExistsSync('./file.txt');
|
||||||
console.log('Virtual directory saved to disk.');
|
|
||||||
|
// Read operations
|
||||||
|
const content = await smartfile.fs.toStringSync('./file.txt');
|
||||||
|
const buffer = await smartfile.fs.toBuffer('./file.txt');
|
||||||
|
const object = await smartfile.fs.toObjectSync('./data.json');
|
||||||
|
|
||||||
|
// Write operations
|
||||||
|
await smartfile.memory.toFs('content', './output.txt');
|
||||||
|
smartfile.memory.toFsSync('content', './output-sync.txt');
|
||||||
|
|
||||||
|
// Copy operations
|
||||||
|
await smartfile.fs.copy('./source.txt', './dest.txt');
|
||||||
|
await smartfile.fs.copy('./src-dir', './dest-dir');
|
||||||
|
|
||||||
|
// Delete operations
|
||||||
|
await smartfile.fs.remove('./file.txt');
|
||||||
|
await smartfile.fs.removeSync('./file-sync.txt');
|
||||||
|
await smartfile.fs.removeMany(['./file1.txt', './file2.txt']);
|
||||||
|
|
||||||
|
// Ensure operations (create if not exists)
|
||||||
|
await smartfile.fs.ensureDir('./my/deep/directory');
|
||||||
|
await smartfile.fs.ensureFile('./my/file.txt');
|
||||||
|
await smartfile.fs.ensureEmptyDir('./empty-dir');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced File Operations
|
### Directory Operations
|
||||||
|
|
||||||
`@push.rocks/smartfile` simplifies complex file operations, including:
|
```typescript
|
||||||
- Copying directories and files
|
// List contents
|
||||||
- Removing files or directories
|
const files = await smartfile.fs.listFiles('./directory');
|
||||||
- Listing files and directories with filters
|
const folders = await smartfile.fs.listFolders('./directory');
|
||||||
- Reading file content directly into JavaScript objects
|
const items = await smartfile.fs.listAllItems('./directory');
|
||||||
|
|
||||||
### Web File Handling
|
// Get file tree
|
||||||
|
const tree = await smartfile.fs.listFileTree('./src', '**/*.ts');
|
||||||
|
|
||||||
Handling files from HTTP requests:
|
// Directory checks
|
||||||
`@push.rocks/smartfile` offers utilities to work with files from web sources, making it simpler to manage downloads and uploads.
|
const isDir = await smartfile.fs.isDirectory('./path');
|
||||||
|
const isFile = await smartfile.fs.isFile('./path');
|
||||||
|
```
|
||||||
|
|
||||||
### Comprehensive File Management
|
### Advanced Features
|
||||||
|
|
||||||
Whether you're dealing with local files, directories, or files over the internet, `@push.rocks/smartfile` provides a comprehensive set of tools to streamline your workflow and reduce the complexity of file management in your Node.js projects.
|
```typescript
|
||||||
|
// Wait for file to be ready
|
||||||
|
await smartfile.fs.waitForFileToBeReady('./file.txt');
|
||||||
|
|
||||||
## API Reference
|
// Stream operations
|
||||||
|
const readStream = smartfile.fsStream.createReadStream('./input.txt');
|
||||||
|
const writeStream = smartfile.fsStream.createWriteStream('./output.txt');
|
||||||
|
|
||||||
### `SmartFile` Class
|
// File type detection
|
||||||
|
const fileType = smartfile.interpreter.filetype('./document.pdf');
|
||||||
|
// Returns: 'pdf'
|
||||||
|
|
||||||
#### Static Methods
|
// Smart read stream (with custom processing)
|
||||||
|
const smartStream = new smartfile.fsStream.SmartReadStream('./data.txt');
|
||||||
|
smartStream.on('data', (chunk) => {
|
||||||
|
// Process chunk
|
||||||
|
console.log(chunk.toString());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
- `SmartFile.fromFilePath(filePath: string, baseArg?: string): Promise<SmartFile>`
|
## 🔄 Working with Multiple Files
|
||||||
- Creates a `SmartFile` instance from a file path.
|
|
||||||
- `SmartFile.fromBuffer(filePath: string, contentBufferArg: Buffer, baseArg?: string): Promise<SmartFile>`
|
|
||||||
- Creates a `SmartFile` instance from a `Buffer`.
|
|
||||||
- `SmartFile.fromString(filePath: string, contentStringArg: string, encodingArg: 'utf8' | 'binary', baseArg?: string): Promise<SmartFile>`
|
|
||||||
- Creates a `SmartFile` instance from a string.
|
|
||||||
- `SmartFile.fromFoldedJson(foldedJsonArg: string): Promise<SmartFile>`
|
|
||||||
- Creates a `SmartFile` instance from a folded JSON string.
|
|
||||||
- `SmartFile.fromStream(stream: Readable, filePath: string, baseArg?: string): Promise<SmartFile>`
|
|
||||||
- Creates a `SmartFile` instance from a `Readable` stream.
|
|
||||||
- `SmartFile.fromUrl(urlArg: string): Promise<SmartFile>`
|
|
||||||
- Creates a `SmartFile` instance from a URL.
|
|
||||||
|
|
||||||
#### Instance Properties
|
```typescript
|
||||||
|
// Process multiple SmartFiles
|
||||||
|
const files = await smartfile.fs.fileTreeToObject(
|
||||||
|
'./src',
|
||||||
|
'**/*.{ts,js}'
|
||||||
|
);
|
||||||
|
|
||||||
- `path: string`
|
// Write array to disk
|
||||||
- The relative path of the file.
|
const smartfiles = [
|
||||||
- `parsedPath: path.ParsedPath`
|
await SmartFile.fromString('file1.txt', 'content1'),
|
||||||
- A parsed path object.
|
await SmartFile.fromString('file2.txt', 'content2')
|
||||||
- `absolutePath: string`
|
];
|
||||||
- The absolute path of the file.
|
await smartfile.memory.smartfileArrayToFs(smartfiles, './output');
|
||||||
- `absoluteParsedPath: path.ParsedPath`
|
```
|
||||||
- A parsed absolute path object.
|
|
||||||
- `contentBuffer: Buffer`
|
|
||||||
- The content of the file as a `Buffer`.
|
|
||||||
- `base: string`
|
|
||||||
- The current working directory of the file.
|
|
||||||
- `sync: boolean`
|
|
||||||
- Indicates whether the file is synced with the disk.
|
|
||||||
|
|
||||||
#### Instance Methods
|
## 🎯 Real-World Examples
|
||||||
|
|
||||||
- `setContentsFromString(contentString: string, encodingArg?: 'utf8' | 'binary'): void`
|
### Website Bundler
|
||||||
- Sets the contents of the file from a string.
|
```typescript
|
||||||
- `write(): Promise<void>`
|
// Bundle website assets
|
||||||
- Writes the file to disk at its original location.
|
const website = await VirtualDirectory.fromFsDirPath('./website');
|
||||||
- `writeToDiskAtPath(filePathArg: string): Promise<void>`
|
const bundle = await website.smartfileArray;
|
||||||
- Writes the file to the specified path on disk.
|
|
||||||
- `writeToDir(dirPathArg: string): Promise<string>`
|
|
||||||
- Writes the file to a directory combined with the relative path portion.
|
|
||||||
- `read(): Promise<void>`
|
|
||||||
- Reads the file from disk.
|
|
||||||
- `delete(): Promise<void>`
|
|
||||||
- Deletes the file from disk at its original location.
|
|
||||||
- `getHash(typeArg?: 'path' | 'content' | 'all'): Promise<string>`
|
|
||||||
- Returns a hash of the file based on the specified type.
|
|
||||||
- `updateFileName(fileNameArg: string): void`
|
|
||||||
- Updates the file name of the `SmartFile` instance.
|
|
||||||
- `editContentAsString(editFuncArg: (fileStringArg: string) => Promise<string>): Promise<void>`
|
|
||||||
- Edits the content of the file as a string using the provided edit function.
|
|
||||||
- `getStream(): Readable`
|
|
||||||
- Returns a `Readable` stream from the file's content buffer.
|
|
||||||
|
|
||||||
### `StreamFile` Class
|
// Process all CSS files
|
||||||
|
for (const file of bundle.filter(f => f.path.endsWith('.css'))) {
|
||||||
|
await file.editContentAsString(async (css) => {
|
||||||
|
// Minify CSS here
|
||||||
|
return css.replace(/\s+/g, ' ');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#### Static Methods
|
// Save processed bundle
|
||||||
|
await website.saveToDisk('./dist');
|
||||||
|
```
|
||||||
|
|
||||||
- `StreamFile.fromPath(filePath: string): Promise<StreamFile>`
|
### File Watcher & Processor
|
||||||
- Creates a `StreamFile` instance from a file path.
|
```typescript
|
||||||
- `StreamFile.fromUrl(url: string): Promise<StreamFile>`
|
// Watch for new files and process them
|
||||||
- Creates a `StreamFile` instance from a URL.
|
import { SmartFile, StreamFile } from '@push.rocks/smartfile';
|
||||||
- `StreamFile.fromBuffer(buffer: Buffer, relativeFilePath?: string): StreamFile`
|
|
||||||
- Creates a `StreamFile` instance from a `Buffer`.
|
|
||||||
- `StreamFile.fromStream(stream: Readable, relativeFilePath?: string, multiUse?: boolean): StreamFile`
|
|
||||||
- Creates a `StreamFile` instance from a `Readable` stream.
|
|
||||||
|
|
||||||
#### Instance Properties
|
async function processLargeFile(filePath: string) {
|
||||||
|
const streamFile = await StreamFile.fromPath(filePath);
|
||||||
|
|
||||||
- `relativeFilePath?: string`
|
// Stream to processed location
|
||||||
- The relative file path of the `StreamFile`.
|
await streamFile.writeToDisk(`./processed/${path.basename(filePath)}`);
|
||||||
- `multiUse: boolean`
|
|
||||||
- Indicates whether the `StreamFile` can be used multiple times.
|
|
||||||
- `used: boolean`
|
|
||||||
- Indicates whether the `StreamFile` has been used.
|
|
||||||
|
|
||||||
#### Instance Methods
|
// Clean up original
|
||||||
|
await smartfile.fs.remove(filePath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- `createReadStream(): Promise<Readable>`
|
### Configuration Manager
|
||||||
- Creates a new `Readable` stream from the source.
|
```typescript
|
||||||
- `writeToDisk(filePathArg: string): Promise<void>`
|
// Load and merge config files
|
||||||
- Writes the stream to the disk at the specified path.
|
const defaultConfig = await smartfile.fs.toObjectSync('./config.default.json');
|
||||||
- `writeToDir(dirPathArg: string): Promise<void>`
|
const userConfig = await smartfile.fs.toObjectSync('./config.user.json');
|
||||||
- Writes the stream to a directory combined with the relative path portion.
|
|
||||||
- `getContentAsBuffer(): Promise<Buffer>`
|
|
||||||
- Returns the content of the `StreamFile` as a `Buffer`.
|
|
||||||
- `getContentAsString(formatArg?: 'utf8' | 'binary'): Promise<string>`
|
|
||||||
- Returns the content of the `StreamFile` as a string.
|
|
||||||
|
|
||||||
### `VirtualDirectory` Class
|
const merged = { ...defaultConfig, ...userConfig };
|
||||||
|
|
||||||
#### Static Methods
|
await smartfile.memory.toFs(
|
||||||
|
JSON.stringify(merged, null, 2),
|
||||||
|
'./config.final.json'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
- `VirtualDirectory.fromFsDirPath(pathArg: string): Promise<VirtualDirectory>`
|
## 🌟 API Reference
|
||||||
- Creates a `VirtualDirectory` instance from a file system directory path.
|
|
||||||
- `VirtualDirectory.fromVirtualDirTransferableObject(virtualDirTransferableObjectArg: VirtualDirTransferableObject): Promise<VirtualDirectory>`
|
|
||||||
- Creates a `VirtualDirectory` instance from a `VirtualDirTransferableObject`.
|
|
||||||
|
|
||||||
#### Instance Properties
|
### Core Modules
|
||||||
|
|
||||||
- `smartfileArray: SmartFile[]`
|
- `fs` - File system operations
|
||||||
- An array of `SmartFile` instances in the `VirtualDirectory`.
|
- `fsStream` - Streaming operations
|
||||||
|
- `memory` - Memory/buffer operations
|
||||||
|
- `interpreter` - File type detection
|
||||||
|
|
||||||
#### Instance Methods
|
### Main Classes
|
||||||
|
|
||||||
- `addSmartfiles(smartfileArrayArg: SmartFile[]): void`
|
- `SmartFile` - Single file representation
|
||||||
- Adds an array of `SmartFile` instances to the `VirtualDirectory`.
|
- `StreamFile` - Streaming file operations
|
||||||
- `getFileByPath(pathArg: string): Promise<SmartFile | undefined>`
|
- `VirtualDirectory` - Virtual filesystem management
|
||||||
- Retrieves a `SmartFile` instance from the `VirtualDirectory` by its path.
|
|
||||||
- `toVirtualDirTransferableObject(): Promise<VirtualDirTransferableObject>`
|
|
||||||
- Converts the `VirtualDirectory` to a `VirtualDirTransferableObject`.
|
|
||||||
- `saveToDisk(dirArg: string): Promise<void>`
|
|
||||||
- Saves the `VirtualDirectory` to the disk at the specified directory.
|
|
||||||
- `shiftToSubdirectory(subDir: string): Promise<VirtualDirectory>`
|
|
||||||
- Shifts the `VirtualDirectory` to a subdirectory.
|
|
||||||
- `addVirtualDirectory(virtualDir: VirtualDirectory, newRoot: string): Promise<void>`
|
|
||||||
- Adds another `VirtualDirectory` to the current `VirtualDirectory` with a new root path.
|
|
||||||
|
|
||||||
### `fs` Module
|
## 🏗️ TypeScript Support
|
||||||
|
|
||||||
The `fs` module provides various file system utility functions. Some notable functions include:
|
Full TypeScript support with comprehensive type definitions:
|
||||||
|
|
||||||
- `fileExistsSync(filePath: string): boolean`
|
```typescript
|
||||||
- Checks if a file exists synchronously.
|
import type { SmartFile, StreamFile, VirtualDirectory } from '@push.rocks/smartfile';
|
||||||
- `fileExists(filePath: string): Promise<boolean>`
|
|
||||||
- Checks if a file exists asynchronously.
|
|
||||||
- `listFoldersSync(pathArg: string, regexFilter?: RegExp): string[]`
|
|
||||||
- Lists folders in a directory synchronously.
|
|
||||||
- `listFolders(pathArg: string, regexFilter?: RegExp): Promise<string[]>`
|
|
||||||
- Lists folders in a directory asynchronously.
|
|
||||||
- `listFilesSync(pathArg: string, regexFilter?: RegExp): string[]`
|
|
||||||
- Lists files in a directory synchronously.
|
|
||||||
- `listFiles(pathArg: string, regexFilter?: RegExp): Promise<string[]>`
|
|
||||||
- Lists files in a directory asynchronously.
|
|
||||||
- `listFileTree(dirPathArg: string, miniMatchFilter: string, absolutePathsBool?: boolean): Promise<string[]>`
|
|
||||||
- Lists a file tree using a minimatch filter.
|
|
||||||
- `waitForFileToBeReady(filePathArg: string): Promise<void>`
|
|
||||||
- Waits for a file to be ready (exists and is not empty).
|
|
||||||
- `toFs(filePathArg: string, fileContentArg: string | Buffer | SmartFile | StreamFile, optionsArg?: { respectRelative?: boolean }): Promise<void>`
|
|
||||||
- Writes a string, `Buffer`, `SmartFile`, or `StreamFile` to the file system.
|
|
||||||
|
|
||||||
### `memory` Module
|
// All methods are fully typed
|
||||||
|
const processFile = async (file: SmartFile): Promise<void> => {
|
||||||
The `memory` module provides utility functions for working with files in memory. Some notable functions include:
|
const content = file.parseContentAsString();
|
||||||
|
// TypeScript knows content is string
|
||||||
- `toObject(fileStringArg: string, fileTypeArg: string): any`
|
};
|
||||||
- Converts a file string to an object based on the file type.
|
```
|
||||||
- `toFs(fileContentArg: string | Buffer | SmartFile | StreamFile, filePathArg: string, optionsArg?: IToFsOptions): Promise<void>`
|
|
||||||
- Writes a string, `Buffer`, `SmartFile`, or `StreamFile` to the file system.
|
|
||||||
- `toFsSync(fileArg: string, filePathArg: string): void`
|
|
||||||
- Writes a string to the file system synchronously.
|
|
||||||
- `smartfileArrayToFs(smartfileArrayArg: SmartFile[], dirArg: string): Promise<void>`
|
|
||||||
- Writes an array of `SmartFile` instances to the file system.
|
|
||||||
|
|
||||||
### `interpreter` Module
|
|
||||||
|
|
||||||
The `interpreter` module provides utility functions for interpreting file types and contents. Some notable functions include:
|
|
||||||
|
|
||||||
- `filetype(pathArg: string): string`
|
|
||||||
- Determines the file type based on the file path.
|
|
||||||
- `objectFile(fileStringArg: string, fileTypeArg: any): any`
|
|
||||||
- Converts a file string to an object based on the file type.
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as smartfile from '../ts/index.js'; // adjust the import path as needed
|
import * as smartfile from '../ts/index.js'; // adjust the import path as needed
|
||||||
|
|
||||||
// Test assets path
|
// Test assets path
|
||||||
@@ -9,55 +9,85 @@ const testAssetsPath = './test/testassets/';
|
|||||||
// StreamFile tests
|
// StreamFile tests
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test('StreamFile.fromPath should create a StreamFile from a file path', async () => {
|
tap.test(
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
|
'StreamFile.fromPath should create a StreamFile from a file path',
|
||||||
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
async () => {
|
||||||
const contentBuffer = await streamFile.getContentAsBuffer();
|
const streamFile = await smartfile.StreamFile.fromPath(
|
||||||
expect(contentBuffer).toBeInstanceOf(Buffer);
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
});
|
);
|
||||||
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
|
const contentBuffer = await streamFile.getContentAsBuffer();
|
||||||
|
expect(contentBuffer).toBeInstanceOf(Buffer);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('StreamFile.fromUrl should create a StreamFile from a URL', async () => {
|
tap.test(
|
||||||
const streamFile = await smartfile.StreamFile.fromUrl('http://example.com/somefile.json');
|
'StreamFile.fromUrl should create a StreamFile from a URL',
|
||||||
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
async () => {
|
||||||
});
|
const streamFile = await smartfile.StreamFile.fromUrl(
|
||||||
|
'http://example.com/somefile.json',
|
||||||
|
);
|
||||||
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('StreamFile.fromBuffer should create a StreamFile from a Buffer', async () => {
|
tap.test(
|
||||||
const buffer = Buffer.from('Some content');
|
'StreamFile.fromBuffer should create a StreamFile from a Buffer',
|
||||||
const streamFile = smartfile.StreamFile.fromBuffer(buffer, 'bufferfile.txt');
|
async () => {
|
||||||
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
const buffer = Buffer.from('Some content');
|
||||||
});
|
const streamFile = smartfile.StreamFile.fromBuffer(
|
||||||
|
buffer,
|
||||||
|
'bufferfile.txt',
|
||||||
|
);
|
||||||
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('StreamFile should write the stream to disk', async () => {
|
tap.test('StreamFile should write the stream to disk', async () => {
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
|
const streamFile = await smartfile.StreamFile.fromPath(
|
||||||
await streamFile.writeToDisk(path.join(testAssetsPath, 'temp', 'mytest.json'));
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
|
);
|
||||||
|
await streamFile.writeToDisk(
|
||||||
|
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
||||||
|
);
|
||||||
// Verify the file was written
|
// Verify the file was written
|
||||||
expect(
|
expect(
|
||||||
// We'll use the fileExists method from your smartfile library
|
// We'll use the fileExists method from your smartfile library
|
||||||
// Replace with the actual method you use to check file existence
|
// Replace with the actual method you use to check file existence
|
||||||
await smartfile.fs.fileExists(path.join(testAssetsPath, 'temp', 'mytest.json'))
|
await smartfile.fs.fileExists(
|
||||||
|
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
||||||
|
),
|
||||||
).toBeTrue();
|
).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('StreamFile should write to a directory', async () => {
|
tap.test('StreamFile should write to a directory', async () => {
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
|
const streamFile = await smartfile.StreamFile.fromPath(
|
||||||
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
|
);
|
||||||
await streamFile.writeToDir(path.join(testAssetsPath, 'temp'));
|
await streamFile.writeToDir(path.join(testAssetsPath, 'temp'));
|
||||||
// Verify the file was written
|
// Verify the file was written
|
||||||
expect(
|
expect(
|
||||||
await smartfile.fs.fileExists(path.join(testAssetsPath, 'temp', 'mytest.json'))
|
await smartfile.fs.fileExists(
|
||||||
|
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
||||||
|
),
|
||||||
).toBeTrue();
|
).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('StreamFile should return content as a buffer', async () => {
|
tap.test('StreamFile should return content as a buffer', async () => {
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
|
const streamFile = await smartfile.StreamFile.fromPath(
|
||||||
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
|
);
|
||||||
const contentBuffer = await streamFile.getContentAsBuffer();
|
const contentBuffer = await streamFile.getContentAsBuffer();
|
||||||
expect(contentBuffer).toBeInstanceOf(Buffer);
|
expect(contentBuffer).toBeInstanceOf(Buffer);
|
||||||
// Further checks on the content can be added here if necessary
|
// Further checks on the content can be added here if necessary
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('StreamFile should return content as a string', async () => {
|
tap.test('StreamFile should return content as a string', async () => {
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
|
const streamFile = await smartfile.StreamFile.fromPath(
|
||||||
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
|
);
|
||||||
const contentString = await streamFile.getContentAsString();
|
const contentString = await streamFile.getContentAsString();
|
||||||
expect(typeof contentString).toBeTypeofString();
|
expect(contentString).toBeTypeofString();
|
||||||
// Verify the content matches what's expected
|
// Verify the content matches what's expected
|
||||||
// This assumes the file contains a JSON object with a key 'key1' with value 'this works'
|
// This assumes the file contains a JSON object with a key 'key1' with value 'this works'
|
||||||
expect(JSON.parse(contentString).key1).toEqual('this works');
|
expect(JSON.parse(contentString).key1).toEqual('this works');
|
||||||
|
315
test/test.ts
315
test/test.ts
@@ -1,85 +1,168 @@
|
|||||||
import * as smartfile from '../ts/index.js';
|
import * as smartfile from '../ts/index.js';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// smartfile.fs
|
// smartfile.fs
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test('.fs.fileExistsSync -> should return an accurate boolean', async () => {
|
tap.test(
|
||||||
// tslint:disable-next-line: no-unused-expression
|
'.fs.fileExistsSync -> should return an accurate boolean',
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/mytest.json')).toBeTrue();
|
async () => {
|
||||||
// tslint:disable-next-line: no-unused-expression
|
// tslint:disable-next-line: no-unused-expression
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/notthere.json')).toBeFalse();
|
expect(
|
||||||
});
|
smartfile.fs.fileExistsSync('./test/testassets/mytest.json'),
|
||||||
|
).toBeTrue();
|
||||||
|
// tslint:disable-next-line: no-unused-expression
|
||||||
|
expect(
|
||||||
|
smartfile.fs.fileExistsSync('./test/testassets/notthere.json'),
|
||||||
|
).toBeFalse();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.fileExists -> should resolve or reject a promise', async () => {
|
tap.test('.fs.fileExists -> should resolve or reject a promise', async () => {
|
||||||
expect(smartfile.fs.fileExists('./test/testassets/mytest.json')).toBeInstanceOf(Promise);
|
await expect(
|
||||||
await smartfile.fs.fileExists('./test/testassets/mytest.json');
|
smartfile.fs.fileExists('./test/testassets/mytest.json'),
|
||||||
await smartfile.fs.fileExists('./test/testassets/notthere.json').catch((err) => {
|
).resolves.toBeTrue();
|
||||||
return expect(err.message).toEqual(
|
await expect(
|
||||||
"ENOENT: no such file or directory, access './test/testassets/notthere.json'"
|
smartfile.fs.fileExists('./test/testassets/notthere.json'),
|
||||||
|
).resolves.toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test(
|
||||||
|
'.fs.listFoldersSync() -> should get the file type from a string',
|
||||||
|
async () => {
|
||||||
|
expect(smartfile.fs.listFoldersSync('./test/testassets/')).toContain(
|
||||||
|
'testfolder',
|
||||||
);
|
);
|
||||||
});
|
expect(smartfile.fs.listFoldersSync('./test/testassets/')).not.toContain(
|
||||||
});
|
'notExistentFolder',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.listFoldersSync() -> should get the file type from a string', async () => {
|
tap.test(
|
||||||
expect(smartfile.fs.listFoldersSync('./test/testassets/')).toContain('testfolder');
|
'.fs.listFolders() -> should get the file type from a string',
|
||||||
expect(smartfile.fs.listFoldersSync('./test/testassets/')).not.toContain('notExistentFolder');
|
async () => {
|
||||||
});
|
const folderArrayArg = await smartfile.fs.listFolders('./test/testassets/');
|
||||||
|
expect(folderArrayArg).toContain('testfolder');
|
||||||
|
expect(folderArrayArg).not.toContain('notExistentFolder');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.listFolders() -> should get the file type from a string', async () => {
|
tap.test(
|
||||||
const folderArrayArg = await smartfile.fs.listFolders('./test/testassets/');
|
'.fs.listFilesSync() -> should get the file type from a string',
|
||||||
expect(folderArrayArg).toContain('testfolder');
|
async () => {
|
||||||
expect(folderArrayArg).not.toContain('notExistentFolder');
|
expect(smartfile.fs.listFilesSync('./test/testassets/')).toContain(
|
||||||
});
|
'mytest.json',
|
||||||
|
);
|
||||||
|
expect(smartfile.fs.listFilesSync('./test/testassets/')).not.toContain(
|
||||||
|
'notExistentFile',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
smartfile.fs.listFilesSync('./test/testassets/', /mytest\.json/),
|
||||||
|
).toContain('mytest.json');
|
||||||
|
expect(
|
||||||
|
smartfile.fs.listFilesSync('./test/testassets/', /mytests.json/),
|
||||||
|
).not.toContain('mytest.json');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.listFilesSync() -> should get the file type from a string', async () => {
|
tap.test(
|
||||||
expect(smartfile.fs.listFilesSync('./test/testassets/')).toContain('mytest.json');
|
'.fs.listFiles() -> should get the file type from a string',
|
||||||
expect(smartfile.fs.listFilesSync('./test/testassets/')).not.toContain('notExistentFile');
|
async () => {
|
||||||
expect(smartfile.fs.listFilesSync('./test/testassets/', /mytest\.json/)).toContain('mytest.json');
|
const folderArrayArg = await smartfile.fs.listFiles('./test/testassets/');
|
||||||
expect(smartfile.fs.listFilesSync('./test/testassets/', /mytests.json/)).not.toContain(
|
expect(folderArrayArg).toContain('mytest.json');
|
||||||
'mytest.json'
|
expect(folderArrayArg).not.toContain('notExistentFile');
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
tap.test('.fs.listFiles() -> should get the file type from a string', async () => {
|
|
||||||
const folderArrayArg = await smartfile.fs.listFiles('./test/testassets/');
|
|
||||||
expect(folderArrayArg).toContain('mytest.json');
|
|
||||||
expect(folderArrayArg).not.toContain('notExistentFile');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('.fs.listFileTree() -> should get a file tree', async () => {
|
tap.test('.fs.listFileTree() -> should get a file tree', async () => {
|
||||||
const folderArrayArg = await smartfile.fs.listFileTree(
|
const folderArrayArg = await smartfile.fs.listFileTree(
|
||||||
path.resolve('./test/testassets/'),
|
path.resolve('./test/testassets/'),
|
||||||
'**/*.txt'
|
'**/*.txt',
|
||||||
);
|
);
|
||||||
expect(folderArrayArg).toContain('testfolder/testfile1.txt');
|
expect(folderArrayArg).toContain('testfolder/testfile1.txt');
|
||||||
expect(folderArrayArg).not.toContain('mytest.json');
|
expect(folderArrayArg).not.toContain('mytest.json');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.fileTreeToObject -> should read a file tree into an Object', async () => {
|
tap.test(
|
||||||
const fileArrayArg = await smartfile.fs.fileTreeToObject(
|
'.fs.listFileTree() -> should find both root and nested .ts files with **/*.ts pattern',
|
||||||
path.resolve('./test/testassets/'),
|
async () => {
|
||||||
'**/*.txt'
|
const tsFiles = await smartfile.fs.listFileTree(process.cwd(), '**/*.ts');
|
||||||
);
|
// Should find both root-level and nested TypeScript files
|
||||||
expect(fileArrayArg[0]).toBeInstanceOf(smartfile.SmartFile);
|
expect(tsFiles).toContain('ts/index.ts');
|
||||||
expect(fileArrayArg[0].contents.toString()).toEqual(fileArrayArg[0].contentBuffer.toString());
|
expect(tsFiles).toContain('ts/classes.smartfile.ts');
|
||||||
});
|
expect(tsFiles).toContain('test/test.ts');
|
||||||
|
// Should find files in multiple levels of nesting
|
||||||
|
expect(tsFiles.filter((f) => f.endsWith('.ts')).length).toBeGreaterThan(5);
|
||||||
|
// Verify it finds files at all levels (root 'ts/' and nested 'test/')
|
||||||
|
const hasRootLevelTs = tsFiles.some(
|
||||||
|
(f) => f.startsWith('ts/') && f.endsWith('.ts'),
|
||||||
|
);
|
||||||
|
const hasNestedTs = tsFiles.some(
|
||||||
|
(f) => f.startsWith('test/') && f.endsWith('.ts'),
|
||||||
|
);
|
||||||
|
expect(hasRootLevelTs).toBeTrue();
|
||||||
|
expect(hasNestedTs).toBeTrue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tap.test(
|
||||||
|
'.fs.listFileTree() -> should handle edge cases with **/ patterns consistently',
|
||||||
|
async () => {
|
||||||
|
// Test that our fix ensures no duplicate files in results
|
||||||
|
const jsonFiles = await smartfile.fs.listFileTree(
|
||||||
|
path.resolve('./test/testassets/'),
|
||||||
|
'**/*.json',
|
||||||
|
);
|
||||||
|
const uniqueFiles = [...new Set(jsonFiles)];
|
||||||
|
expect(jsonFiles.length).toEqual(uniqueFiles.length);
|
||||||
|
|
||||||
|
// Test that it finds root level files with **/ patterns
|
||||||
|
const txtFiles = await smartfile.fs.listFileTree(
|
||||||
|
path.resolve('./test/testassets/'),
|
||||||
|
'**/*.txt',
|
||||||
|
);
|
||||||
|
// Should include both direct files and nested files
|
||||||
|
expect(txtFiles).toContain('mytest.txt');
|
||||||
|
expect(txtFiles).toContain('testfolder/testfile1.txt');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tap.test(
|
||||||
|
'.fs.fileTreeToObject -> should read a file tree into an Object',
|
||||||
|
async () => {
|
||||||
|
const fileArrayArg = await smartfile.fs.fileTreeToObject(
|
||||||
|
path.resolve('./test/testassets/'),
|
||||||
|
'**/*.txt',
|
||||||
|
);
|
||||||
|
expect(fileArrayArg[0]).toBeInstanceOf(smartfile.SmartFile);
|
||||||
|
expect(fileArrayArg[0].contents.toString()).toEqual(
|
||||||
|
fileArrayArg[0].contentBuffer.toString(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.copy() -> should copy a directory', async () => {
|
tap.test('.fs.copy() -> should copy a directory', async () => {
|
||||||
await smartfile.fs.copy('./test/testassets/testfolder/', './test/testassets/temp/');
|
await smartfile.fs.copy(
|
||||||
|
'./test/testassets/testfolder/',
|
||||||
|
'./test/testassets/temp/',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.copy() -> should copy a file', async () => {
|
tap.test('.fs.copy() -> should copy a file', async () => {
|
||||||
await smartfile.fs.copy('./test/testassets/mytest.yaml', './test/testassets/temp/mytest.yaml');
|
await smartfile.fs.copy(
|
||||||
|
'./test/testassets/mytest.yaml',
|
||||||
|
'./test/testassets/temp/mytest.yaml',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.copy() -> should copy a file and rename it', async () => {
|
tap.test('.fs.copy() -> should copy a file and rename it', async () => {
|
||||||
await smartfile.fs.copy(
|
await smartfile.fs.copy(
|
||||||
'./test/testassets/mytest.yaml',
|
'./test/testassets/mytest.yaml',
|
||||||
'./test/testassets/temp/mytestRenamed.yaml'
|
'./test/testassets/temp/mytestRenamed.yaml',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,70 +172,105 @@ tap.test('.fs.remove -> should remove single files', async () => {
|
|||||||
await smartfile.fs.remove('./test/testassets/temp/mytestRenamed.yaml');
|
await smartfile.fs.remove('./test/testassets/temp/mytestRenamed.yaml');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.removeSync -> should remove single files synchronouly', async () => {
|
tap.test(
|
||||||
smartfile.fs.removeSync('./test/testassets/temp/testfile1.txt');
|
'.fs.removeSync -> should remove single files synchronouly',
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
|
async () => {
|
||||||
});
|
smartfile.fs.removeSync('./test/testassets/temp/testfile1.txt');
|
||||||
|
expect(
|
||||||
|
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
|
||||||
|
).toBeFalse();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.removeMany -> should remove and array of files', async () => {
|
tap.test('.fs.removeMany -> should remove and array of files', async () => {
|
||||||
smartfile.fs
|
smartfile.fs
|
||||||
.removeMany(['./test/testassets/temp/testfile1.txt', './test/testassets/temp/testfile2.txt'])
|
.removeMany([
|
||||||
|
'./test/testassets/temp/testfile1.txt',
|
||||||
|
'./test/testassets/temp/testfile2.txt',
|
||||||
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
|
expect(
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt')).toBeFalse();
|
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
|
||||||
|
).toBeFalse();
|
||||||
|
expect(
|
||||||
|
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt'),
|
||||||
|
).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.removeManySync -> should remove and array of single files synchronouly', async () => {
|
tap.test(
|
||||||
smartfile.fs.removeManySync([
|
'.fs.removeManySync -> should remove and array of single files synchronouly',
|
||||||
'./test/testassets/temp/testfile1.txt',
|
async () => {
|
||||||
'./test/testassets/temp/testfile2.txt',
|
smartfile.fs.removeManySync([
|
||||||
]);
|
'./test/testassets/temp/testfile1.txt',
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
|
'./test/testassets/temp/testfile2.txt',
|
||||||
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt')).toBeFalse();
|
]);
|
||||||
});
|
expect(
|
||||||
|
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
|
||||||
|
).toBeFalse();
|
||||||
|
expect(
|
||||||
|
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt'),
|
||||||
|
).toBeFalse();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.toObjectSync() -> should read an .yaml file to an object', async () => {
|
tap.test(
|
||||||
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.yaml');
|
'.fs.toObjectSync() -> should read an .yaml file to an object',
|
||||||
expect(testData.key1).toEqual('this works');
|
async () => {
|
||||||
expect(testData.key2).toEqual('this works too');
|
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.yaml');
|
||||||
});
|
expect(testData.key1).toEqual('this works');
|
||||||
|
expect(testData.key2).toEqual('this works too');
|
||||||
|
},
|
||||||
|
);
|
||||||
tap.test(
|
tap.test(
|
||||||
'.fs.toObjectSync() -> should state unknown file type for unknown file types',
|
'.fs.toObjectSync() -> should state unknown file type for unknown file types',
|
||||||
async () => {
|
async () => {
|
||||||
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.txt');
|
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.txt');
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
tap.test('.fs.toObjectSync() -> should read an .json file to an object', async () => {
|
tap.test(
|
||||||
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.json');
|
'.fs.toObjectSync() -> should read an .json file to an object',
|
||||||
expect(testData.key1).toEqual('this works');
|
async () => {
|
||||||
expect(testData.key2).toEqual('this works too');
|
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.json');
|
||||||
});
|
expect(testData.key1).toEqual('this works');
|
||||||
|
expect(testData.key2).toEqual('this works too');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('.fs.toStringSync() -> should read a file to a string', async () => {
|
tap.test('.fs.toStringSync() -> should read a file to a string', async () => {
|
||||||
expect(smartfile.fs.toStringSync('./test/testassets/mytest.txt')).toEqual('Some TestString &&%$');
|
expect(smartfile.fs.toStringSync('./test/testassets/mytest.txt')).toEqual(
|
||||||
|
'Some TestString &&%$',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// smartfile.interpreter
|
// smartfile.interpreter
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test('.interpreter.filetype() -> should get the file type from a string', async () => {
|
tap.test(
|
||||||
expect(smartfile.interpreter.filetype('./somefolder/data.json')).toEqual('json');
|
'.interpreter.filetype() -> should get the file type from a string',
|
||||||
});
|
async () => {
|
||||||
|
expect(smartfile.interpreter.filetype('./somefolder/data.json')).toEqual(
|
||||||
|
'json',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// smartfile.memory
|
// smartfile.memory
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test('.memory.toFs() -> should write a file to disk and return a promise', async () => {
|
tap.test(
|
||||||
const localString = 'myString';
|
'.memory.toFs() -> should write a file to disk and return a promise',
|
||||||
await smartfile.memory.toFs(
|
async () => {
|
||||||
localString,
|
const localString = 'myString';
|
||||||
path.join(process.cwd(), './test/testassets/temp/testMemToFs.txt')
|
await smartfile.memory.toFs(
|
||||||
);
|
localString,
|
||||||
});
|
path.join(process.cwd(), './test/testassets/temp/testMemToFs.txt'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test(
|
tap.test(
|
||||||
'.memory.toFsSync() -> should write a file to disk and return true if successfull',
|
'.memory.toFsSync() -> should write a file to disk and return true if successfull',
|
||||||
@@ -160,9 +278,9 @@ tap.test(
|
|||||||
const localString = 'myString';
|
const localString = 'myString';
|
||||||
smartfile.memory.toFsSync(
|
smartfile.memory.toFsSync(
|
||||||
localString,
|
localString,
|
||||||
path.join(process.cwd(), './test/testassets/temp/testMemToFsSync.txt')
|
path.join(process.cwd(), './test/testassets/temp/testMemToFsSync.txt'),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -172,7 +290,7 @@ tap.test(
|
|||||||
tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
|
tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
|
||||||
const smartfileArray = await smartfile.fs.fileTreeToObject(
|
const smartfileArray = await smartfile.fs.fileTreeToObject(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'./test/testassets/testfolder/**/*'
|
'./test/testassets/testfolder/**/*',
|
||||||
);
|
);
|
||||||
const localSmartfile = smartfileArray[0];
|
const localSmartfile = smartfileArray[0];
|
||||||
expect(localSmartfile).toBeInstanceOf(smartfile.SmartFile);
|
expect(localSmartfile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
@@ -186,7 +304,10 @@ tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should output a smartfile array to disk', async () => {
|
tap.test('should output a smartfile array to disk', async () => {
|
||||||
const smartfileArray = await smartfile.fs.fileTreeToObject('./test/testassets/testfolder/', '*');
|
const smartfileArray = await smartfile.fs.fileTreeToObject(
|
||||||
|
'./test/testassets/testfolder/',
|
||||||
|
'*',
|
||||||
|
);
|
||||||
for (const smartfileInstance of smartfileArray) {
|
for (const smartfileInstance of smartfileArray) {
|
||||||
console.log(smartfileInstance.relative);
|
console.log(smartfileInstance.relative);
|
||||||
console.log(smartfileInstance.path);
|
console.log(smartfileInstance.path);
|
||||||
@@ -195,14 +316,18 @@ tap.test('should output a smartfile array to disk', async () => {
|
|||||||
}
|
}
|
||||||
await smartfile.memory.smartfileArrayToFs(
|
await smartfile.memory.smartfileArrayToFs(
|
||||||
smartfileArray,
|
smartfileArray,
|
||||||
path.resolve('./test/testassets/temp/testoutput/')
|
path.resolve('./test/testassets/temp/testoutput/'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create, store and retrieve valid smartfiles', async () => {
|
tap.test('should create, store and retrieve valid smartfiles', async () => {
|
||||||
const fileString = 'hi there';
|
const fileString = 'hi there';
|
||||||
const filePath = './test/testassets/utf8.txt';
|
const filePath = './test/testassets/utf8.txt';
|
||||||
const smartfileInstance = await smartfile.SmartFile.fromString(filePath, fileString, 'utf8');
|
const smartfileInstance = await smartfile.SmartFile.fromString(
|
||||||
|
filePath,
|
||||||
|
fileString,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
smartfileInstance.write();
|
smartfileInstance.write();
|
||||||
const smartfileInstance2 = await smartfile.SmartFile.fromFilePath(filePath);
|
const smartfileInstance2 = await smartfile.SmartFile.fromFilePath(filePath);
|
||||||
const retrievedString = smartfileInstance.contents.toString();
|
const retrievedString = smartfileInstance.contents.toString();
|
||||||
@@ -212,7 +337,11 @@ tap.test('should create, store and retrieve valid smartfiles', async () => {
|
|||||||
tap.test('should get a hash', async () => {
|
tap.test('should get a hash', async () => {
|
||||||
const fileString = 'hi there';
|
const fileString = 'hi there';
|
||||||
const filePath = './test/testassets/utf8.txt';
|
const filePath = './test/testassets/utf8.txt';
|
||||||
const smartfileInstance = await smartfile.SmartFile.fromString(filePath, fileString, 'utf8');
|
const smartfileInstance = await smartfile.SmartFile.fromString(
|
||||||
|
filePath,
|
||||||
|
fileString,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
const hash = await smartfileInstance.getHash();
|
const hash = await smartfileInstance.getHash();
|
||||||
console.log(hash);
|
console.log(hash);
|
||||||
});
|
});
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
import * as smartfile from '../ts/index.js';
|
import * as smartfile from '../ts/index.js';
|
||||||
|
|
||||||
tap.test('should create a virtualdirectory', async () => {
|
tap.test('should create a virtualdirectory', async () => {
|
||||||
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath('./test/testassets/testfolder');
|
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
|
||||||
|
'./test/testassets/testfolder',
|
||||||
|
);
|
||||||
expect(virtualDir.smartfileArray.length).toEqual(4);
|
expect(virtualDir.smartfileArray.length).toEqual(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should write to a directory', async () => {
|
tap.test('should write to a directory', async () => {
|
||||||
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath('./test/testassets/testfolder');
|
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
|
||||||
|
'./test/testassets/testfolder',
|
||||||
|
);
|
||||||
virtualDir.saveToDisk('./test/testassets/test');
|
virtualDir.saveToDisk('./test/testassets/test');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -5,4 +5,3 @@
|
|||||||
"nestedkey1": "hello"
|
"nestedkey1": "hello"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,4 +5,3 @@
|
|||||||
"nestedkey1": "hello"
|
"nestedkey1": "hello"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,4 +5,3 @@
|
|||||||
"nestedkey1": "hello"
|
"nestedkey1": "hello"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* autocreated commitinfo by @pushrocks/commitinfo
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartfile',
|
name: '@push.rocks/smartfile',
|
||||||
version: '11.0.11',
|
version: '11.2.6',
|
||||||
description: 'provides a robust suite of tools for managing files in Node.js using TypeScript.'
|
description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.'
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* creates a Smartfile from a filePath
|
* creates a Smartfile from a filePath
|
||||||
* @param filePath
|
* @param filePath
|
||||||
*/
|
*/
|
||||||
public static async fromFilePath(filePath: string, baseArg: string = process.cwd()) {
|
public static async fromFilePath(
|
||||||
|
filePath: string,
|
||||||
|
baseArg: string = process.cwd(),
|
||||||
|
) {
|
||||||
filePath = plugins.path.resolve(filePath);
|
filePath = plugins.path.resolve(filePath);
|
||||||
const fileBuffer = fs.toBufferSync(filePath);
|
const fileBuffer = fs.toBufferSync(filePath);
|
||||||
const smartfile = new SmartFile({
|
const smartfile = new SmartFile({
|
||||||
@@ -34,7 +37,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
public static async fromBuffer(
|
public static async fromBuffer(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
contentBufferArg: Buffer,
|
contentBufferArg: Buffer,
|
||||||
baseArg: string = process.cwd()
|
baseArg: string = process.cwd(),
|
||||||
) {
|
) {
|
||||||
const smartfile = new SmartFile({
|
const smartfile = new SmartFile({
|
||||||
contentBuffer: contentBufferArg,
|
contentBuffer: contentBufferArg,
|
||||||
@@ -49,7 +52,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
contentStringArg: string,
|
contentStringArg: string,
|
||||||
encodingArg: 'utf8' | 'binary',
|
encodingArg: 'utf8' | 'binary',
|
||||||
baseArg = process.cwd()
|
baseArg = process.cwd(),
|
||||||
) {
|
) {
|
||||||
const smartfile = new SmartFile({
|
const smartfile = new SmartFile({
|
||||||
contentBuffer: Buffer.from(contentStringArg, encodingArg),
|
contentBuffer: Buffer.from(contentStringArg, encodingArg),
|
||||||
@@ -73,7 +76,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
public static async fromStream(
|
public static async fromStream(
|
||||||
stream: plugins.stream.Readable,
|
stream: plugins.stream.Readable,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
baseArg: string = process.cwd()
|
baseArg: string = process.cwd(),
|
||||||
): Promise<SmartFile> {
|
): Promise<SmartFile> {
|
||||||
return new Promise<SmartFile>((resolve, reject) => {
|
return new Promise<SmartFile>((resolve, reject) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
@@ -91,9 +94,13 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromUrl (urlArg: string) {
|
public static async fromUrl(urlArg: string) {
|
||||||
const response = await plugins.smartrequest.getBinary(urlArg);
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
const smartfile = await SmartFile.fromBuffer(urlArg, response.body);
|
.url(urlArg)
|
||||||
|
.accept('binary')
|
||||||
|
.get();
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
const smartfile = await SmartFile.fromBuffer(urlArg, buffer);
|
||||||
return smartfile;
|
return smartfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +166,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* set contents from string
|
* set contents from string
|
||||||
* @param contentString
|
* @param contentString
|
||||||
*/
|
*/
|
||||||
public setContentsFromString(contentString: string, encodingArg: 'utf8' | 'binary' = 'utf8') {
|
public setContentsFromString(
|
||||||
|
contentString: string,
|
||||||
|
encodingArg: 'utf8' | 'binary' = 'utf8',
|
||||||
|
) {
|
||||||
this.contents = Buffer.from(contentString, encodingArg);
|
this.contents = Buffer.from(contentString, encodingArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +179,11 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* - no argument write to exactly where the file was picked up
|
* - no argument write to exactly where the file was picked up
|
||||||
*/
|
*/
|
||||||
public async write() {
|
public async write() {
|
||||||
let writePath = plugins.path.join(this.base, this.path);
|
let writePath = plugins.smartpath.transform.makeAbsolute(
|
||||||
plugins.smartpath.transform.toAbsolute(this.path, this.base);
|
this.path,
|
||||||
|
this.base,
|
||||||
|
);
|
||||||
|
console.log(`writing to ${writePath}`);
|
||||||
await memory.toFs(this.contentBuffer, writePath);
|
await memory.toFs(this.contentBuffer, writePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +215,9 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* read file from disk
|
* read file from disk
|
||||||
*/
|
*/
|
||||||
public async read() {
|
public async read() {
|
||||||
this.contentBuffer = await fs.toBuffer(plugins.path.join(this.base, this.path));
|
this.contentBuffer = await fs.toBuffer(
|
||||||
|
plugins.path.join(this.base, this.path),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,6 +227,52 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
await fs.remove(plugins.path.join(this.base, this.path));
|
await fs.remove(plugins.path.join(this.base, this.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the file to the specified new name.
|
||||||
|
* - Updates the `path` property with the new name.
|
||||||
|
* - Writes the file to the new location if it exists on disk.
|
||||||
|
* @param newName The new name of the file (including extension if applicable).
|
||||||
|
* @param writeToDisk (optional) If true, also renames the file on the disk.
|
||||||
|
* @returns The updated file path after renaming.
|
||||||
|
*/
|
||||||
|
public async rename(
|
||||||
|
newName: string,
|
||||||
|
writeToDisk: boolean = false,
|
||||||
|
): Promise<string> {
|
||||||
|
// Validate the new name
|
||||||
|
if (!newName || typeof newName !== 'string') {
|
||||||
|
throw new Error('Invalid new name provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the directory path
|
||||||
|
const oldFilePath = this.path;
|
||||||
|
const dirPath = plugins.path.dirname(this.path);
|
||||||
|
|
||||||
|
// Create the new file path
|
||||||
|
const newFilePath = plugins.path.join(dirPath, newName);
|
||||||
|
|
||||||
|
// Update the `path` property
|
||||||
|
this.path = newFilePath;
|
||||||
|
|
||||||
|
// Optionally write the renamed file to disk
|
||||||
|
if (writeToDisk) {
|
||||||
|
const oldAbsolutePath = plugins.smartpath.transform.makeAbsolute(
|
||||||
|
oldFilePath,
|
||||||
|
this.base,
|
||||||
|
);
|
||||||
|
const newAbsolutePath = plugins.smartpath.transform.makeAbsolute(
|
||||||
|
newFilePath,
|
||||||
|
this.base,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rename the file on disk
|
||||||
|
await plugins.fsExtra.rename(oldAbsolutePath, newAbsolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new path
|
||||||
|
return this.path;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------
|
// -----------------------------------------------
|
||||||
// vinyl compatibility
|
// vinyl compatibility
|
||||||
// -----------------------------------------------
|
// -----------------------------------------------
|
||||||
@@ -273,8 +334,12 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
|
|
||||||
public async getHash(typeArg: 'path' | 'content' | 'all' = 'all') {
|
public async getHash(typeArg: 'path' | 'content' | 'all' = 'all') {
|
||||||
const pathHash = await plugins.smarthash.sha256FromString(this.path);
|
const pathHash = await plugins.smarthash.sha256FromString(this.path);
|
||||||
const contentHash = await plugins.smarthash.sha256FromBuffer(this.contentBuffer);
|
const contentHash = await plugins.smarthash.sha256FromBuffer(
|
||||||
const combinedHash = await plugins.smarthash.sha256FromString(pathHash + contentHash);
|
this.contentBuffer,
|
||||||
|
);
|
||||||
|
const combinedHash = await plugins.smarthash.sha256FromString(
|
||||||
|
pathHash + contentHash,
|
||||||
|
);
|
||||||
switch (typeArg) {
|
switch (typeArg) {
|
||||||
case 'path':
|
case 'path':
|
||||||
return pathHash;
|
return pathHash;
|
||||||
@@ -292,7 +357,9 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
this.path = this.path.replace(new RegExp(oldFileName + '$'), fileNameArg);
|
this.path = this.path.replace(new RegExp(oldFileName + '$'), fileNameArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async editContentAsString(editFuncArg: (fileStringArg: string) => Promise<string>) {
|
public async editContentAsString(
|
||||||
|
editFuncArg: (fileStringArg: string) => Promise<string>,
|
||||||
|
) {
|
||||||
const newFileString = await editFuncArg(this.contentBuffer.toString());
|
const newFileString = await editFuncArg(this.contentBuffer.toString());
|
||||||
this.contentBuffer = Buffer.from(newFileString);
|
this.contentBuffer = Buffer.from(newFileString);
|
||||||
}
|
}
|
||||||
@@ -306,4 +373,11 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
stream.push(null); // Push null to signify the end of the stream (EOF)
|
stream.push(null); // Push null to signify the end of the stream (EOF)
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the file in bytes
|
||||||
|
*/
|
||||||
|
public async getSize(): Promise<number> {
|
||||||
|
return this.contentBuffer.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,44 +3,51 @@ import * as smartfileFs from './fs.js';
|
|||||||
import * as smartfileFsStream from './fsstream.js';
|
import * as smartfileFsStream from './fsstream.js';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
type TStreamSource = (streamFile: StreamFile) => Promise<Readable>;
|
type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The StreamFile class represents a file as a stream.
|
* The StreamFile class represents a file as a stream.
|
||||||
* It allows creating streams from a file path, a URL, or a buffer.
|
* It allows creating streams from a file path, a URL, or a buffer.
|
||||||
*/
|
*/
|
||||||
export class StreamFile {
|
export class StreamFile {
|
||||||
// INSTANCE
|
|
||||||
relativeFilePath?: string;
|
|
||||||
private streamSource: TStreamSource;
|
|
||||||
|
|
||||||
// enable stream based multi use
|
|
||||||
private cachedStreamBuffer?: Buffer;
|
|
||||||
public multiUse: boolean;
|
|
||||||
public used: boolean = false;
|
|
||||||
|
|
||||||
private constructor(streamSource: TStreamSource, relativeFilePath?: string) {
|
|
||||||
this.streamSource = streamSource;
|
|
||||||
this.relativeFilePath = relativeFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// STATIC
|
// STATIC
|
||||||
|
|
||||||
public static async fromPath(filePath: string): Promise<StreamFile> {
|
public static async fromPath(filePath: string): Promise<StreamFile> {
|
||||||
const streamSource: TStreamSource = async (stremFileArg) => smartfileFsStream.createReadStream(filePath);
|
const streamSource: TStreamSource = async (streamFileArg) =>
|
||||||
|
smartfileFsStream.createReadStream(filePath);
|
||||||
const streamFile = new StreamFile(streamSource, filePath);
|
const streamFile = new StreamFile(streamSource, filePath);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
|
streamFile.byteLengthComputeFunction = async () => {
|
||||||
|
const stats = await smartfileFs.stat(filePath);
|
||||||
|
return stats.size;
|
||||||
|
};
|
||||||
return streamFile;
|
return streamFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromUrl(url: string): Promise<StreamFile> {
|
public static async fromUrl(url: string): Promise<StreamFile> {
|
||||||
const streamSource: TStreamSource = async (streamFileArg) => plugins.smartrequest.getStream(url); // Replace with actual plugin method
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
||||||
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
|
.url(url)
|
||||||
|
.get();
|
||||||
|
return response.stream();
|
||||||
|
};
|
||||||
const streamFile = new StreamFile(streamSource);
|
const streamFile = new StreamFile(streamSource);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
|
streamFile.byteLengthComputeFunction = async () => {
|
||||||
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
|
.url(url)
|
||||||
|
.accept('binary')
|
||||||
|
.get();
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
return buffer.length;
|
||||||
|
};
|
||||||
return streamFile;
|
return streamFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromBuffer(buffer: Buffer, relativeFilePath?: string): StreamFile {
|
public static fromBuffer(
|
||||||
|
buffer: Buffer,
|
||||||
|
relativeFilePath?: string,
|
||||||
|
): StreamFile {
|
||||||
const streamSource: TStreamSource = async (streamFileArg) => {
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream.push(buffer);
|
stream.push(buffer);
|
||||||
@@ -49,6 +56,7 @@ export class StreamFile {
|
|||||||
};
|
};
|
||||||
const streamFile = new StreamFile(streamSource, relativeFilePath);
|
const streamFile = new StreamFile(streamSource, relativeFilePath);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
|
streamFile.byteLengthComputeFunction = async () => buffer.length;
|
||||||
return streamFile;
|
return streamFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +67,11 @@ export class StreamFile {
|
|||||||
* @param multiUse If true, the stream can be read multiple times, caching its content.
|
* @param multiUse If true, the stream can be read multiple times, caching its content.
|
||||||
* @returns A StreamFile instance.
|
* @returns A StreamFile instance.
|
||||||
*/
|
*/
|
||||||
public static fromStream(stream: Readable, relativeFilePath?: string, multiUse: boolean = false): StreamFile {
|
public static fromStream(
|
||||||
|
stream: Readable,
|
||||||
|
relativeFilePath?: string,
|
||||||
|
multiUse: boolean = false,
|
||||||
|
): StreamFile {
|
||||||
const streamSource: TStreamSource = (streamFileArg) => {
|
const streamSource: TStreamSource = (streamFileArg) => {
|
||||||
if (streamFileArg.multiUse) {
|
if (streamFileArg.multiUse) {
|
||||||
// If multi-use is enabled and we have cached content, create a new readable stream from the buffer
|
// If multi-use is enabled and we have cached content, create a new readable stream from the buffer
|
||||||
@@ -91,6 +103,21 @@ export class StreamFile {
|
|||||||
return streamFile;
|
return streamFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
relativeFilePath?: string;
|
||||||
|
private streamSource: TStreamSource;
|
||||||
|
|
||||||
|
// enable stream based multi use
|
||||||
|
private cachedStreamBuffer?: Buffer;
|
||||||
|
public multiUse: boolean;
|
||||||
|
public used: boolean = false;
|
||||||
|
public byteLengthComputeFunction: () => Promise<number>;
|
||||||
|
|
||||||
|
private constructor(streamSource: TStreamSource, relativeFilePath?: string) {
|
||||||
|
this.streamSource = streamSource;
|
||||||
|
this.relativeFilePath = relativeFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
// METHODS
|
// METHODS
|
||||||
|
|
||||||
private checkMultiUse() {
|
private checkMultiUse() {
|
||||||
@@ -104,7 +131,16 @@ export class StreamFile {
|
|||||||
* Creates a new readable stream from the source.
|
* Creates a new readable stream from the source.
|
||||||
*/
|
*/
|
||||||
public async createReadStream(): Promise<Readable> {
|
public async createReadStream(): Promise<Readable> {
|
||||||
return this.streamSource(this);
|
const stream = await this.streamSource(this);
|
||||||
|
|
||||||
|
// Check if it's a Web ReadableStream and convert to Node.js Readable
|
||||||
|
if (stream && typeof (stream as any).getReader === 'function') {
|
||||||
|
// This is a Web ReadableStream, convert it to Node.js Readable
|
||||||
|
return Readable.fromWeb(stream as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's already a Node.js Readable stream
|
||||||
|
return stream as Readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,4 +185,15 @@ export class StreamFile {
|
|||||||
const contentBuffer = await this.getContentAsBuffer();
|
const contentBuffer = await this.getContentAsBuffer();
|
||||||
return contentBuffer.toString(formatArg);
|
return contentBuffer.toString(formatArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the file content in bytes.
|
||||||
|
*/
|
||||||
|
public async getSize(): Promise<number> {
|
||||||
|
if (this.byteLengthComputeFunction) {
|
||||||
|
return this.byteLengthComputeFunction();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,33 +2,33 @@ import { SmartFile } from './classes.smartfile.js';
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as fs from './fs.js';
|
import * as fs from './fs.js';
|
||||||
|
|
||||||
|
|
||||||
export interface IVirtualDirectoryConstructorOptions {
|
export interface IVirtualDirectoryConstructorOptions {
|
||||||
mode: ''
|
mode: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a virtual directory exposes a fs api
|
* a virtual directory exposes a fs api
|
||||||
*/
|
*/
|
||||||
export class VirtualDirectory {
|
export class VirtualDirectory {
|
||||||
|
consstructor(options = {}) {}
|
||||||
consstructor(options = {}) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// STATIC
|
// STATIC
|
||||||
public static async fromFsDirPath(pathArg: string): Promise<VirtualDirectory> {
|
public static async fromFsDirPath(
|
||||||
|
pathArg: string,
|
||||||
|
): Promise<VirtualDirectory> {
|
||||||
const newVirtualDir = new VirtualDirectory();
|
const newVirtualDir = new VirtualDirectory();
|
||||||
newVirtualDir.addSmartfiles(await fs.fileTreeToObject(pathArg, '**/*'));
|
newVirtualDir.addSmartfiles(await fs.fileTreeToObject(pathArg, '**/*'));
|
||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromVirtualDirTransferableObject(
|
public static async fromVirtualDirTransferableObject(
|
||||||
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject
|
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject,
|
||||||
): Promise<VirtualDirectory> {
|
): Promise<VirtualDirectory> {
|
||||||
const newVirtualDir = new VirtualDirectory();
|
const newVirtualDir = new VirtualDirectory();
|
||||||
for (const fileArg of virtualDirTransferableObjectArg.files) {
|
for (const fileArg of virtualDirTransferableObjectArg.files) {
|
||||||
newVirtualDir.addSmartfiles([SmartFile.enfoldFromJson(fileArg) as SmartFile]);
|
newVirtualDir.addSmartfiles([
|
||||||
|
SmartFile.enfoldFromJson(fileArg) as SmartFile,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,9 @@ export class VirtualDirectory {
|
|||||||
|
|
||||||
public async toVirtualDirTransferableObject(): Promise<plugins.smartfileInterfaces.VirtualDirTransferableObject> {
|
public async toVirtualDirTransferableObject(): Promise<plugins.smartfileInterfaces.VirtualDirTransferableObject> {
|
||||||
return {
|
return {
|
||||||
files: this.smartfileArray.map((smartfileArg) => smartfileArg.foldToJson()),
|
files: this.smartfileArray.map((smartfileArg) =>
|
||||||
|
smartfileArg.foldToJson(),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +80,10 @@ export class VirtualDirectory {
|
|||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addVirtualDirectory(virtualDir: VirtualDirectory, newRoot: string): Promise<void> {
|
public async addVirtualDirectory(
|
||||||
|
virtualDir: VirtualDirectory,
|
||||||
|
newRoot: string,
|
||||||
|
): Promise<void> {
|
||||||
for (const file of virtualDir.smartfileArray) {
|
for (const file of virtualDir.smartfileArray) {
|
||||||
file.path = plugins.path.join(newRoot, file.path);
|
file.path = plugins.path.join(newRoot, file.path);
|
||||||
}
|
}
|
||||||
|
370
ts/fs.ts
370
ts/fs.ts
@@ -72,25 +72,47 @@ export const isFile = (pathArg): boolean => {
|
|||||||
===============================================================*/
|
===============================================================*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* copies a file from A to B on the local disk
|
* copies a file or directory from A to B on the local disk
|
||||||
*/
|
*/
|
||||||
export const copy = async (fromArg: string, toArg: string): Promise<boolean> => {
|
export const copy = async (
|
||||||
const done = plugins.smartpromise.defer<boolean>();
|
fromArg: string,
|
||||||
plugins.fsExtra.copy(fromArg, toArg, {}, (err) => {
|
toArg: string,
|
||||||
if (err) {
|
optionsArg?: plugins.fsExtra.CopyOptions & { replaceTargetDir?: boolean },
|
||||||
throw new Error(`Could not copy from ${fromArg} to ${toArg}: ${err}`);
|
): Promise<void> => {
|
||||||
}
|
if (
|
||||||
done.resolve(true);
|
optionsArg?.replaceTargetDir &&
|
||||||
});
|
isDirectory(fromArg) &&
|
||||||
return done.promise;
|
isDirectory(toArg)
|
||||||
|
) {
|
||||||
|
await remove(toArg);
|
||||||
|
}
|
||||||
|
return await plugins.fsExtra.copy(
|
||||||
|
fromArg,
|
||||||
|
toArg,
|
||||||
|
optionsArg as plugins.fsExtra.CopyOptions,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* copies a file SYNCHRONOUSLY from A to B on the local disk
|
* copies a file or directory SYNCHRONOUSLY from A to B on the local disk
|
||||||
*/
|
*/
|
||||||
export const copySync = (fromArg: string, toArg: string): boolean => {
|
export const copySync = (
|
||||||
plugins.fsExtra.copySync(fromArg, toArg);
|
fromArg: string,
|
||||||
return true;
|
toArg: string,
|
||||||
|
optionsArg?: plugins.fsExtra.CopyOptionsSync & { replaceTargetDir?: boolean },
|
||||||
|
): void => {
|
||||||
|
if (
|
||||||
|
optionsArg?.replaceTargetDir &&
|
||||||
|
isDirectory(fromArg) &&
|
||||||
|
isDirectory(toArg)
|
||||||
|
) {
|
||||||
|
removeSync(toArg);
|
||||||
|
}
|
||||||
|
return plugins.fsExtra.copySync(
|
||||||
|
fromArg,
|
||||||
|
toArg,
|
||||||
|
optionsArg as plugins.fsExtra.CopyOptionsSync,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +154,10 @@ export const ensureEmptyDirSync = (dirPathArg: string) => {
|
|||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
* @exec ASYNC
|
* @exec ASYNC
|
||||||
*/
|
*/
|
||||||
export const ensureFile = async (filePathArg, initFileStringArg): Promise<void> => {
|
export const ensureFile = async (
|
||||||
|
filePathArg,
|
||||||
|
initFileStringArg,
|
||||||
|
): Promise<void> => {
|
||||||
ensureFileSync(filePathArg, initFileStringArg);
|
ensureFileSync(filePathArg, initFileStringArg);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,7 +168,10 @@ export const ensureFile = async (filePathArg, initFileStringArg): Promise<void>
|
|||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
* @exec SYNC
|
* @exec SYNC
|
||||||
*/
|
*/
|
||||||
export const ensureFileSync = (filePathArg: string, initFileStringArg: string): void => {
|
export const ensureFileSync = (
|
||||||
|
filePathArg: string,
|
||||||
|
initFileStringArg: string,
|
||||||
|
): void => {
|
||||||
if (fileExistsSync(filePathArg)) {
|
if (fileExistsSync(filePathArg)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@@ -190,7 +218,8 @@ export const removeManySync = (filePathArrayArg: string[]): void => {
|
|||||||
===============================================================*/
|
===============================================================*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* reads a file content to an object
|
||||||
|
* good for JSON, YAML, TOML, etc.
|
||||||
* @param filePathArg
|
* @param filePathArg
|
||||||
* @param fileTypeArg
|
* @param fileTypeArg
|
||||||
* @returns {any}
|
* @returns {any}
|
||||||
@@ -198,16 +227,26 @@ export const removeManySync = (filePathArrayArg: string[]): void => {
|
|||||||
export const toObjectSync = (filePathArg, fileTypeArg?) => {
|
export const toObjectSync = (filePathArg, fileTypeArg?) => {
|
||||||
const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
|
const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
|
||||||
let fileType;
|
let fileType;
|
||||||
fileTypeArg ? (fileType = fileTypeArg) : (fileType = interpreter.filetype(filePathArg));
|
fileTypeArg
|
||||||
return interpreter.objectFile(fileString, fileType);
|
? (fileType = fileTypeArg)
|
||||||
|
: (fileType = interpreter.filetype(filePathArg));
|
||||||
|
try {
|
||||||
|
return interpreter.objectFile(fileString, fileType);
|
||||||
|
} catch (err) {
|
||||||
|
err.message = `Failed to read file at ${filePathArg}` + err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* reads a file content to a String
|
* reads a file content to a String
|
||||||
*/
|
*/
|
||||||
export const toStringSync = (filePath: string): string => {
|
export const toStringSync = (filePath: string): string => {
|
||||||
const encoding = plugins.smartmime.getEncoding(filePath);
|
const encoding = plugins.smartmime.getEncodingForPathSync(filePath);
|
||||||
let fileString: string | Buffer = plugins.fsExtra.readFileSync(filePath, encoding);
|
let fileString: string | Buffer = plugins.fsExtra.readFileSync(
|
||||||
|
filePath,
|
||||||
|
encoding,
|
||||||
|
);
|
||||||
if (Buffer.isBuffer(fileString)) {
|
if (Buffer.isBuffer(fileString)) {
|
||||||
fileString = fileString.toString('binary');
|
fileString = fileString.toString('binary');
|
||||||
}
|
}
|
||||||
@@ -234,7 +273,10 @@ export const toReadStream = (filePath: string): plugins.fs.ReadStream => {
|
|||||||
return plugins.fsExtra.createReadStream(filePath);
|
return plugins.fsExtra.createReadStream(filePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string) => {
|
export const fileTreeToHash = async (
|
||||||
|
dirPathArg: string,
|
||||||
|
miniMatchFilter: string,
|
||||||
|
) => {
|
||||||
const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
|
const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
|
||||||
let combinedString = '';
|
let combinedString = '';
|
||||||
for (const smartfile of fileTreeObject) {
|
for (const smartfile of fileTreeObject) {
|
||||||
@@ -249,7 +291,10 @@ export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string
|
|||||||
* @param dirPathArg the directory to start from
|
* @param dirPathArg the directory to start from
|
||||||
* @param miniMatchFilter a minimatch filter of what files to include
|
* @param miniMatchFilter a minimatch filter of what files to include
|
||||||
*/
|
*/
|
||||||
export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: string) => {
|
export const fileTreeToObject = async (
|
||||||
|
dirPathArg: string,
|
||||||
|
miniMatchFilter: string,
|
||||||
|
) => {
|
||||||
// handle absolute miniMatchFilter
|
// handle absolute miniMatchFilter
|
||||||
let dirPath: string;
|
let dirPath: string;
|
||||||
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
||||||
@@ -276,7 +321,7 @@ export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: stri
|
|||||||
contentBuffer: fileBuffer,
|
contentBuffer: fileBuffer,
|
||||||
base: dirPath,
|
base: dirPath,
|
||||||
path: filePath,
|
path: filePath,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return smartfileArray;
|
return smartfileArray;
|
||||||
@@ -286,7 +331,10 @@ export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: stri
|
|||||||
* lists Folders in a directory on local disk
|
* lists Folders in a directory on local disk
|
||||||
* @returns Promise with an array that contains the folder names
|
* @returns Promise with an array that contains the folder names
|
||||||
*/
|
*/
|
||||||
export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
export const listFolders = async (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): Promise<string[]> => {
|
||||||
return listFoldersSync(pathArg, regexFilter);
|
return listFoldersSync(pathArg, regexFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,9 +342,14 @@ export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promis
|
|||||||
* lists Folders SYNCHRONOUSLY in a directory on local disk
|
* lists Folders SYNCHRONOUSLY in a directory on local disk
|
||||||
* @returns an array with the folder names as strings
|
* @returns an array with the folder names as strings
|
||||||
*/
|
*/
|
||||||
export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
export const listFoldersSync = (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): string[] => {
|
||||||
let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||||||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isDirectory();
|
return plugins.fsExtra
|
||||||
|
.statSync(plugins.path.join(pathArg, file))
|
||||||
|
.isDirectory();
|
||||||
});
|
});
|
||||||
if (regexFilter) {
|
if (regexFilter) {
|
||||||
folderArray = folderArray.filter((fileItem) => {
|
folderArray = folderArray.filter((fileItem) => {
|
||||||
@@ -310,7 +363,10 @@ export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[]
|
|||||||
* lists Files in a directory on local disk
|
* lists Files in a directory on local disk
|
||||||
* @returns Promise
|
* @returns Promise
|
||||||
*/
|
*/
|
||||||
export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
export const listFiles = async (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): Promise<string[]> => {
|
||||||
return listFilesSync(pathArg, regexFilter);
|
return listFilesSync(pathArg, regexFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,7 +374,10 @@ export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<
|
|||||||
* lists Files SYNCHRONOUSLY in a directory on local disk
|
* lists Files SYNCHRONOUSLY in a directory on local disk
|
||||||
* @returns an array with the folder names as strings
|
* @returns an array with the folder names as strings
|
||||||
*/
|
*/
|
||||||
export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
export const listFilesSync = (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): string[] => {
|
||||||
let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||||||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
||||||
});
|
});
|
||||||
@@ -334,7 +393,10 @@ export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] =
|
|||||||
* lists all items (folders AND files) in a directory on local disk
|
* lists all items (folders AND files) in a directory on local disk
|
||||||
* @returns Promise<string[]>
|
* @returns Promise<string[]>
|
||||||
*/
|
*/
|
||||||
export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
export const listAllItems = async (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): Promise<string[]> => {
|
||||||
return listAllItemsSync(pathArg, regexFilter);
|
return listAllItemsSync(pathArg, regexFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,7 +405,10 @@ export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promi
|
|||||||
* @returns an array with the folder names as strings
|
* @returns an array with the folder names as strings
|
||||||
* @executes SYNC
|
* @executes SYNC
|
||||||
*/
|
*/
|
||||||
export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
export const listAllItemsSync = (
|
||||||
|
pathArg: string,
|
||||||
|
regexFilter?: RegExp,
|
||||||
|
): string[] => {
|
||||||
let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||||||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
||||||
});
|
});
|
||||||
@@ -363,7 +428,7 @@ export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[
|
|||||||
export const listFileTree = async (
|
export const listFileTree = async (
|
||||||
dirPathArg: string,
|
dirPathArg: string,
|
||||||
miniMatchFilter: string,
|
miniMatchFilter: string,
|
||||||
absolutePathsBool: boolean = false
|
absolutePathsBool: boolean = false,
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
// handle absolute miniMatchFilter
|
// handle absolute miniMatchFilter
|
||||||
let dirPath: string;
|
let dirPath: string;
|
||||||
@@ -379,7 +444,28 @@ export const listFileTree = async (
|
|||||||
dot: true,
|
dot: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fileList = await plugins.glob.glob(miniMatchFilter, options);
|
// Fix inconsistent **/* glob behavior across systems
|
||||||
|
// Some glob implementations don't include root-level files when using **/*
|
||||||
|
// To ensure consistent behavior, we expand **/* patterns to include both root and nested files
|
||||||
|
let patterns: string[];
|
||||||
|
if (miniMatchFilter.startsWith('**/')) {
|
||||||
|
// Extract the part after **/ (e.g., "*.ts" from "**/*.ts")
|
||||||
|
const rootPattern = miniMatchFilter.substring(3);
|
||||||
|
// Use both the root pattern and the original pattern to ensure we catch everything
|
||||||
|
patterns = [rootPattern, miniMatchFilter];
|
||||||
|
} else {
|
||||||
|
patterns = [miniMatchFilter];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results from all patterns
|
||||||
|
const allFiles = new Set<string>();
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const files = await plugins.glob.glob(pattern, options);
|
||||||
|
files.forEach((file) => allFiles.add(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileList = Array.from(allFiles).sort();
|
||||||
|
|
||||||
if (absolutePathsBool) {
|
if (absolutePathsBool) {
|
||||||
fileList = fileList.map((filePath) => {
|
fileList = fileList.map((filePath) => {
|
||||||
return plugins.path.resolve(plugins.path.join(dirPath, filePath));
|
return plugins.path.resolve(plugins.path.join(dirPath, filePath));
|
||||||
@@ -391,53 +477,187 @@ export const listFileTree = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for file stability before resolving the promise.
|
* Watches for file stability before resolving the promise.
|
||||||
|
* Ensures that the directory/file exists before setting up the watcher.
|
||||||
|
*
|
||||||
|
* **New behavior**: If the given path is a directory, this function will:
|
||||||
|
* 1. Wait for that directory to exist (creating a timeout if needed).
|
||||||
|
* 2. Watch the directory until at least one file appears.
|
||||||
|
* 3. Then wait for the first file in the directory to stabilize before resolving.
|
||||||
|
*
|
||||||
|
* @param fileOrDirPathArg The path of the file or directory to monitor.
|
||||||
|
* @param timeoutMs The maximum time to wait for the file to stabilize (in milliseconds). Default is 60 seconds.
|
||||||
|
* @returns A promise that resolves when the target is stable or rejects on timeout/error.
|
||||||
*/
|
*/
|
||||||
export const waitForFileToBeReady = (filePathArg: string): Promise<void> => {
|
export const waitForFileToBeReady = async (
|
||||||
return new Promise(async (resolve, reject) => {
|
fileOrDirPathArg: string,
|
||||||
|
timeoutMs: number = 60000,
|
||||||
|
): Promise<void> => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a path (file or directory) exists. If it doesn't yet exist,
|
||||||
|
* wait until it does (or time out).
|
||||||
|
* @param pathToCheck The file or directory path to check.
|
||||||
|
*/
|
||||||
|
const ensurePathExists = async (pathToCheck: string): Promise<void> => {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await plugins.smartpromise.fromCallback((cb) =>
|
||||||
|
plugins.fs.access(pathToCheck, plugins.fs.constants.F_OK, cb),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err; // Propagate unexpected errors
|
||||||
|
}
|
||||||
|
if (Date.now() - startTime > timeoutMs) {
|
||||||
|
throw new Error(`Timeout waiting for path to exist: ${pathToCheck}`);
|
||||||
|
}
|
||||||
|
await plugins.smartdelay.delayFor(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a file (not directory) is stable by comparing sizes
|
||||||
|
* across successive checks.
|
||||||
|
* @param filePathArg The path of the file to check.
|
||||||
|
* @returns A promise that resolves once the file stops changing.
|
||||||
|
*/
|
||||||
|
const waitForSingleFileToBeStable = async (
|
||||||
|
filePathArg: string,
|
||||||
|
): Promise<void> => {
|
||||||
let lastFileSize = -1;
|
let lastFileSize = -1;
|
||||||
let fileIsStable = false;
|
let fileIsStable = false;
|
||||||
|
|
||||||
|
// We'll create a helper for repeated stats-checking logic
|
||||||
const checkFileStability = async () => {
|
const checkFileStability = async () => {
|
||||||
let currentFileSize: number;
|
try {
|
||||||
const deferred = plugins.smartpromise.defer();
|
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>(
|
||||||
plugins.fs.stat(filePathArg, (err, stats) => {
|
(cb) => plugins.fs.stat(filePathArg, cb),
|
||||||
if (err) {
|
);
|
||||||
fileIsStable = true;
|
if (stats.isDirectory()) {
|
||||||
watcher.close();
|
// If it unexpectedly turns out to be a directory here, throw
|
||||||
reject(err);
|
throw new Error(
|
||||||
return;
|
`Expected a file but found a directory: ${filePathArg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stats.size === lastFileSize) {
|
||||||
|
fileIsStable = true;
|
||||||
|
} else {
|
||||||
|
lastFileSize = stats.size;
|
||||||
|
fileIsStable = false;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// Ignore only if file not found
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
currentFileSize = stats.size;
|
|
||||||
deferred.resolve();
|
|
||||||
});
|
|
||||||
await deferred.promise;
|
|
||||||
if (currentFileSize === lastFileSize) {
|
|
||||||
fileIsStable = true;
|
|
||||||
await plugins.smartdelay.delayFor(100);
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
lastFileSize = currentFileSize;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const watcher = plugins.fs.watch(filePathArg, (eventType, filename) => {
|
// Ensure file exists first
|
||||||
if (eventType === 'change') {
|
await ensurePathExists(filePathArg);
|
||||||
checkFileStability();
|
|
||||||
|
// Set up a watcher on the file itself
|
||||||
|
const fileWatcher = plugins.fs.watch(
|
||||||
|
filePathArg,
|
||||||
|
{ persistent: true },
|
||||||
|
async () => {
|
||||||
|
if (!fileIsStable) {
|
||||||
|
await checkFileStability();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Poll until stable or timeout
|
||||||
|
while (!fileIsStable) {
|
||||||
|
if (Date.now() - startTime > timeoutMs) {
|
||||||
|
throw new Error(
|
||||||
|
`Timeout waiting for file to stabilize: ${filePathArg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await checkFileStability();
|
||||||
|
if (!fileIsStable) {
|
||||||
|
await plugins.smartdelay.delayFor(1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
} finally {
|
||||||
|
fileWatcher.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watcher.on('error', (error) => {
|
/**
|
||||||
watcher.close();
|
* Main logic: check if we have a directory or file at fileOrDirPathArg.
|
||||||
reject(error);
|
* If directory, wait for first file in the directory to appear and stabilize.
|
||||||
});
|
* If file, do the old single-file wait logic.
|
||||||
|
*/
|
||||||
|
const statsForGivenPath = await (async () => {
|
||||||
|
try {
|
||||||
|
await ensurePathExists(fileOrDirPathArg);
|
||||||
|
return await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||||||
|
plugins.fs.stat(fileOrDirPathArg, cb),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// If there's an error (including timeout), just rethrow
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
while (!fileIsStable) {
|
if (!statsForGivenPath.isDirectory()) {
|
||||||
await checkFileStability();
|
// It's a file – just do the single-file stability wait
|
||||||
if (!fileIsStable) {
|
await waitForSingleFileToBeStable(fileOrDirPathArg);
|
||||||
await plugins.smartdelay.delayFor(5000);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, it's a directory. Wait for the first file inside to appear and be stable
|
||||||
|
const dirPath = fileOrDirPathArg;
|
||||||
|
|
||||||
|
// Helper to find the first file in the directory if it exists
|
||||||
|
const getFirstFileInDirectory = async (): Promise<string | null> => {
|
||||||
|
const entries = await plugins.smartpromise.fromCallback<string[]>((cb) =>
|
||||||
|
plugins.fs.readdir(dirPath, cb),
|
||||||
|
);
|
||||||
|
// We only want actual files, not subdirectories
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryPath = plugins.path.join(dirPath, entry);
|
||||||
|
const entryStats =
|
||||||
|
await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||||||
|
plugins.fs.stat(entryPath, cb),
|
||||||
|
);
|
||||||
|
if (entryStats.isFile()) {
|
||||||
|
return entryPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
watcher.close();
|
return null;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Wait for a file to appear in this directory
|
||||||
|
let firstFilePath = await getFirstFileInDirectory();
|
||||||
|
if (!firstFilePath) {
|
||||||
|
// Set up a watcher on the directory to see if a file appears
|
||||||
|
const directoryWatcher = plugins.fs.watch(dirPath, { persistent: true });
|
||||||
|
try {
|
||||||
|
// We'll poll for the existence of a file in that directory
|
||||||
|
while (!firstFilePath) {
|
||||||
|
if (Date.now() - startTime > timeoutMs) {
|
||||||
|
throw new Error(
|
||||||
|
`Timeout waiting for a file to appear in directory: ${dirPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
firstFilePath = await getFirstFileInDirectory();
|
||||||
|
if (!firstFilePath) {
|
||||||
|
await plugins.smartdelay.delayFor(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
directoryWatcher.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have a file path, wait for that file to stabilize
|
||||||
|
await waitForSingleFileToBeStable(firstFilePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -447,11 +667,11 @@ export const waitForFileToBeReady = (filePathArg: string): Promise<void> => {
|
|||||||
* @param fileBaseArg
|
* @param fileBaseArg
|
||||||
*/
|
*/
|
||||||
export let toFs = async (
|
export let toFs = async (
|
||||||
filePathArg: string,
|
|
||||||
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
||||||
|
filePathArg: string,
|
||||||
optionsArg: {
|
optionsArg: {
|
||||||
respectRelative?: boolean;
|
respectRelative?: boolean;
|
||||||
} = {}
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
const done = plugins.smartpromise.defer();
|
const done = plugins.smartpromise.defer();
|
||||||
|
|
||||||
@@ -481,11 +701,15 @@ export let toFs = async (
|
|||||||
throw new Error('fileContent is neither string nor Smartfile');
|
throw new Error('fileContent is neither string nor Smartfile');
|
||||||
}
|
}
|
||||||
await ensureDir(plugins.path.parse(filePath).dir);
|
await ensureDir(plugins.path.parse(filePath).dir);
|
||||||
plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
|
plugins.fsExtra.writeFile(
|
||||||
|
filePath,
|
||||||
|
fileContent,
|
||||||
|
{ encoding: fileEncoding },
|
||||||
|
done.resolve,
|
||||||
|
);
|
||||||
return await done.promise;
|
return await done.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const stat = async (filePathArg: string) => {
|
||||||
|
return plugins.fsPromises.stat(filePathArg);
|
||||||
|
};
|
||||||
|
|
||||||
|
@@ -13,17 +13,17 @@ export const createWriteStream = (pathArg: string) => {
|
|||||||
|
|
||||||
export const processFile = async (
|
export const processFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>
|
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fileStream = createReadStream(filePath);
|
const fileStream = createReadStream(filePath);
|
||||||
asyncFunc(fileStream).then(resolve).catch(reject);
|
asyncFunc(fileStream).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const processDirectory = async (
|
export const processDirectory = async (
|
||||||
directoryPath: string,
|
directoryPath: string,
|
||||||
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>
|
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const files = plugins.fs.readdirSync(directoryPath, { withFileTypes: true });
|
const files = plugins.fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -41,12 +41,15 @@ export const processDirectory = async (
|
|||||||
/**
|
/**
|
||||||
* Checks if a file is ready to be streamed (exists and is not empty).
|
* Checks if a file is ready to be streamed (exists and is not empty).
|
||||||
*/
|
*/
|
||||||
export const isFileReadyForStreaming = async (filePathArg: string): Promise<boolean> => {
|
export const isFileReadyForStreaming = async (
|
||||||
|
filePathArg: string,
|
||||||
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const stats = await plugins.fs.promises.stat(filePathArg);
|
const stats = await plugins.fs.promises.stat(filePathArg);
|
||||||
return stats.size > 0;
|
return stats.size > 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') { // File does not exist
|
if (error.code === 'ENOENT') {
|
||||||
|
// File does not exist
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw error; // Rethrow other unexpected errors
|
throw error; // Rethrow other unexpected errors
|
||||||
@@ -56,7 +59,9 @@ export const isFileReadyForStreaming = async (filePathArg: string): Promise<bool
|
|||||||
/**
|
/**
|
||||||
* Waits for a file to be ready for streaming (exists and is not empty).
|
* Waits for a file to be ready for streaming (exists and is not empty).
|
||||||
*/
|
*/
|
||||||
export const waitForFileToBeReadyForStreaming = (filePathArg: string): Promise<void> => {
|
export const waitForFileToBeReadyForStreaming = (
|
||||||
|
filePathArg: string,
|
||||||
|
): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Normalize and resolve the file path
|
// Normalize and resolve the file path
|
||||||
const filePath = plugins.path.resolve(filePathArg);
|
const filePath = plugins.path.resolve(filePathArg);
|
||||||
@@ -80,11 +85,15 @@ export const waitForFileToBeReadyForStreaming = (filePathArg: string): Promise<v
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Set up file watcher
|
// Set up file watcher
|
||||||
const watcher = plugins.fs.watch(filePath, { persistent: false }, (eventType) => {
|
const watcher = plugins.fs.watch(
|
||||||
if (eventType === 'change' || eventType === 'rename') {
|
filePath,
|
||||||
checkFile(resolve, reject);
|
{ persistent: false },
|
||||||
}
|
(eventType) => {
|
||||||
});
|
if (eventType === 'change' || eventType === 'rename') {
|
||||||
|
checkFile(resolve, reject);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Check file immediately in case it's already ready
|
// Check file immediately in case it's already ready
|
||||||
checkFile(resolve, reject);
|
checkFile(resolve, reject);
|
||||||
@@ -105,7 +114,11 @@ export class SmartReadStream extends plugins.stream.Readable {
|
|||||||
private endDelay: number;
|
private endDelay: number;
|
||||||
private reading: boolean = false;
|
private reading: boolean = false;
|
||||||
|
|
||||||
constructor(filePath: string, endDelay = 60000, opts?: plugins.stream.ReadableOptions) {
|
constructor(
|
||||||
|
filePath: string,
|
||||||
|
endDelay = 60000,
|
||||||
|
opts?: plugins.stream.ReadableOptions,
|
||||||
|
) {
|
||||||
super(opts);
|
super(opts);
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
this.endDelay = endDelay;
|
this.endDelay = endDelay;
|
||||||
@@ -165,26 +178,33 @@ export class SmartReadStream extends plugins.stream.Readable {
|
|||||||
this.emit('error', err);
|
this.emit('error', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
plugins.fs.read(fd, buffer, 0, chunkSize, this.lastReadSize, (err, bytesRead, buffer) => {
|
plugins.fs.read(
|
||||||
if (err) {
|
fd,
|
||||||
this.emit('error', err);
|
buffer,
|
||||||
return;
|
0,
|
||||||
}
|
chunkSize,
|
||||||
|
this.lastReadSize,
|
||||||
if (bytesRead > 0) {
|
(err, bytesRead, buffer) => {
|
||||||
this.lastReadSize += bytesRead;
|
|
||||||
this.push(buffer.slice(0, bytesRead)); // Push the data onto the stream
|
|
||||||
} else {
|
|
||||||
this.reading = false; // No more data to read for now
|
|
||||||
this.resetEndTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins.fs.close(fd, (err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.emit('error', err);
|
this.emit('error', err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
if (bytesRead > 0) {
|
||||||
|
this.lastReadSize += bytesRead;
|
||||||
|
this.push(buffer.slice(0, bytesRead)); // Push the data onto the stream
|
||||||
|
} else {
|
||||||
|
this.reading = false; // No more data to read for now
|
||||||
|
this.resetEndTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.fs.close(fd, (err) => {
|
||||||
|
if (err) {
|
||||||
|
this.emit('error', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
ts/memory.ts
14
ts/memory.ts
@@ -27,7 +27,7 @@ export interface IToFsOptions {
|
|||||||
export let toFs = async (
|
export let toFs = async (
|
||||||
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
||||||
filePathArg: string,
|
filePathArg: string,
|
||||||
optionsArg: IToFsOptions = {}
|
optionsArg: IToFsOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const done = plugins.smartpromise.defer();
|
const done = plugins.smartpromise.defer();
|
||||||
|
|
||||||
@@ -57,7 +57,12 @@ export let toFs = async (
|
|||||||
throw new Error('fileContent is neither string nor Smartfile');
|
throw new Error('fileContent is neither string nor Smartfile');
|
||||||
}
|
}
|
||||||
await smartfileFs.ensureDir(plugins.path.parse(filePath).dir);
|
await smartfileFs.ensureDir(plugins.path.parse(filePath).dir);
|
||||||
plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
|
plugins.fsExtra.writeFile(
|
||||||
|
filePath,
|
||||||
|
fileContent,
|
||||||
|
{ encoding: fileEncoding },
|
||||||
|
done.resolve,
|
||||||
|
);
|
||||||
return await done.promise;
|
return await done.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,7 +89,10 @@ export const toFsSync = (fileArg: string, filePathArg: string) => {
|
|||||||
plugins.fsExtra.writeFileSync(filePath, fileString, { encoding: 'utf8' });
|
plugins.fsExtra.writeFileSync(filePath, fileString, { encoding: 'utf8' });
|
||||||
};
|
};
|
||||||
|
|
||||||
export let smartfileArrayToFs = async (smartfileArrayArg: SmartFile[], dirArg: string) => {
|
export let smartfileArrayToFs = async (
|
||||||
|
smartfileArrayArg: SmartFile[],
|
||||||
|
dirArg: string,
|
||||||
|
) => {
|
||||||
await smartfileFs.ensureDir(dirArg);
|
await smartfileFs.ensureDir(dirArg);
|
||||||
for (const smartfile of smartfileArrayArg) {
|
for (const smartfile of smartfileArrayArg) {
|
||||||
await toFs(smartfile, dirArg, {
|
await toFs(smartfile, dirArg, {
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
// node native scope
|
// node native scope
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as fsPromises from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as stream from 'stream';
|
import * as stream from 'stream';
|
||||||
|
|
||||||
export { fs, path, stream };
|
export { fs, fsPromises, path, stream };
|
||||||
|
|
||||||
// @pushrocks scope
|
// @pushrocks scope
|
||||||
import * as lik from '@push.rocks/lik';
|
import * as lik from '@push.rocks/lik';
|
||||||
|
@@ -6,9 +6,9 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user