Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f5eb4a4d4 | |||
| ad33cb6d73 | |||
| 16d47ea348 | |||
| dc92b7fe93 |
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-11-22 - 13.0.0 - BREAKING CHANGE(SmartFileFactory)
|
||||||
|
Refactor to in-memory file API and introduce SmartFileFactory; delegate filesystem operations to @push.rocks/smartfs; bump to 12.0.0
|
||||||
|
|
||||||
|
- Introduce SmartFileFactory as the canonical entry point for creating SmartFile, StreamFile and VirtualDirectory instances.
|
||||||
|
- Refactor SmartFile, StreamFile and VirtualDirectory to be in-memory representations and accept an optional SmartFs instance for filesystem operations.
|
||||||
|
- Delegate low-level filesystem operations to @push.rocks/smartfs (added as a peerDependency); legacy fs/memory/fsStream/interpreter namespace exports removed/deprecated.
|
||||||
|
- Add StreamFile.toSmartFile(), VirtualDirectory.loadFromDisk(), and other convenience methods to work with the new factory/smartFs integration.
|
||||||
|
- Update tests to use a MockSmartFs and the factory API; add test assets.
|
||||||
|
- Documentation and readme updated with migration instructions and examples showing SmartFileFactory.nodeFs() and SmartFs usage.
|
||||||
|
- Bumped package version to 12.0.0 — this is a breaking change; consumers should migrate from legacy namespace exports to the factory + @push.rocks/smartfs workflow.
|
||||||
|
|
||||||
|
## 2025-08-18 - 11.2.7 - fix(ci)
|
||||||
|
Remove .npmrc containing hard-coded npm registry configuration
|
||||||
|
|
||||||
|
- Removed .npmrc which contained 'registry=https://registry.npmjs.org/'
|
||||||
|
- Avoids committing environment-specific npm registry configuration; rely on user or CI environment settings instead
|
||||||
|
|
||||||
## 2025-08-18 - 11.2.6 - fix(fs)
|
## 2025-08-18 - 11.2.6 - fix(fs)
|
||||||
Improve fs and stream handling, enhance SmartFile/StreamFile, update tests and CI configs
|
Improve fs and stream handling, enhance SmartFile/StreamFile, update tests and CI configs
|
||||||
|
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartfile",
|
"name": "@push.rocks/smartfile",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.2.6",
|
"version": "13.0.0",
|
||||||
"description": "Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.",
|
"description": "High-level file representation classes (SmartFile, StreamFile, VirtualDirectory) for efficient in-memory file management in Node.js using TypeScript. Works seamlessly with @push.rocks/smartfs for filesystem 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",
|
||||||
@@ -19,21 +19,17 @@
|
|||||||
"file management",
|
"file management",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"Node.js",
|
"Node.js",
|
||||||
"file operations",
|
"in-memory files",
|
||||||
"file manipulation",
|
"SmartFile",
|
||||||
|
"StreamFile",
|
||||||
|
"VirtualDirectory",
|
||||||
|
"file representation",
|
||||||
"file streaming",
|
"file streaming",
|
||||||
"virtual directory",
|
"virtual directory",
|
||||||
"filesystem utilities",
|
"file factory",
|
||||||
"memory-efficient file handling",
|
"memory-efficient file handling",
|
||||||
"custom file operations",
|
"buffer operations",
|
||||||
"write files",
|
"file content manipulation"
|
||||||
"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",
|
||||||
@@ -58,6 +54,14 @@
|
|||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"js-yaml": "^4.1.0"
|
"js-yaml": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@push.rocks/smartfs": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@push.rocks/smartfs": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.4",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
|
|||||||
135
readme.hints.md
135
readme.hints.md
@@ -1,5 +1,140 @@
|
|||||||
# SmartFile Implementation Hints
|
# SmartFile Implementation Hints
|
||||||
|
|
||||||
|
## Major Architectural Change (v12.0.0)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
SmartFile has been refactored to focus exclusively on **in-memory file representations** (SmartFile, StreamFile, VirtualDirectory). All filesystem operations have been moved to or delegated to `@push.rocks/smartfs`.
|
||||||
|
|
||||||
|
### Key Changes
|
||||||
|
|
||||||
|
1. **Factory Pattern Introduction**
|
||||||
|
- New `SmartFileFactory` class introduced
|
||||||
|
- Factory is bound to a `SmartFs` instance (from `@push.rocks/smartfs`)
|
||||||
|
- All file instances are created through the factory
|
||||||
|
- Factory methods: `fromFilePath()`, `fromUrl()`, `fromBuffer()`, `fromString()`, etc.
|
||||||
|
|
||||||
|
2. **SmartFile, StreamFile, VirtualDirectory**
|
||||||
|
- Now accept optional `smartFs` parameter in constructor
|
||||||
|
- Filesystem operations (write, read, delete) use `smartFs` if available
|
||||||
|
- Fallback to legacy methods if `smartFs` not provided (for backward compatibility)
|
||||||
|
- Static factory methods moved to `SmartFileFactory`
|
||||||
|
|
||||||
|
3. **Separation of Concerns**
|
||||||
|
- **SmartFile** = In-memory file representation (path + content buffer)
|
||||||
|
- **StreamFile** = Lazy-loaded streaming file representation
|
||||||
|
- **VirtualDirectory** = Collection of SmartFiles in memory
|
||||||
|
- **SmartFs** (from @push.rocks/smartfs) = Filesystem operations
|
||||||
|
|
||||||
|
### Usage Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
|
|
||||||
|
// Create factory with SmartFs instance
|
||||||
|
const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
const factory = new SmartFileFactory(smartFs);
|
||||||
|
|
||||||
|
// Or use default Node.js factory
|
||||||
|
const factory = SmartFileFactory.nodeFs();
|
||||||
|
|
||||||
|
// Create SmartFile through factory
|
||||||
|
const file = await factory.fromFilePath('./data.json');
|
||||||
|
await file.write(); // Uses bound smartFs instance
|
||||||
|
|
||||||
|
// Create StreamFile
|
||||||
|
const stream = await factory.streamFromPath('./large.zip');
|
||||||
|
|
||||||
|
// Create VirtualDirectory
|
||||||
|
const vdir = await factory.virtualDirectoryFromPath('./src');
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Belongs Where
|
||||||
|
|
||||||
|
**SmartFile/StreamFile/VirtualDirectory (this package)**:
|
||||||
|
- ✅ In-memory file representation
|
||||||
|
- ✅ Content manipulation (edit, parse, transform)
|
||||||
|
- ✅ Loading content FROM sources (factory methods)
|
||||||
|
- ✅ Saving content TO destinations (write methods)
|
||||||
|
- ✅ Instance metadata (hash, size, mime type)
|
||||||
|
- ✅ Collection operations (for VirtualDirectory)
|
||||||
|
|
||||||
|
**SmartFs (@push.rocks/smartfs)**:
|
||||||
|
- ✅ Filesystem queries (exists, stat)
|
||||||
|
- ✅ File operations without content loading (copy, move)
|
||||||
|
- ✅ Directory operations (list, create, delete)
|
||||||
|
- ✅ Streaming operations (readStream, writeStream)
|
||||||
|
- ✅ Provider abstraction (Node.js, memory, S3, etc.)
|
||||||
|
|
||||||
|
### VirtualDirectory Collection Methods
|
||||||
|
|
||||||
|
VirtualDirectory now has comprehensive collection methods:
|
||||||
|
|
||||||
|
**Queries** (operate on in-memory collection):
|
||||||
|
- `exists(path)` / `has(path)` - Check if path exists in collection
|
||||||
|
- `getFileByPath(path)` - Get SmartFile from collection
|
||||||
|
- `listFiles()` - List all SmartFiles
|
||||||
|
- `listDirectories()` - List directory paths represented in collection
|
||||||
|
- `filter(predicate)` - Filter SmartFiles
|
||||||
|
- `map(fn)` - Transform SmartFiles
|
||||||
|
- `find(predicate)` - Find SmartFile
|
||||||
|
- `size()` - Number of files in collection
|
||||||
|
- `isEmpty()` - Check if collection is empty
|
||||||
|
|
||||||
|
**Mutations**:
|
||||||
|
- `addSmartfiles(files)` - Add files to collection
|
||||||
|
- `addSmartfile(file)` - Add single file
|
||||||
|
- `removeByPath(path)` - Remove from collection
|
||||||
|
- `clear()` - Empty collection
|
||||||
|
- `merge(otherVDir)` - Merge another VirtualDirectory
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
- Legacy namespace exports (`fs`, `memory`, `fsStream`, `interpreter`) are **deprecated**
|
||||||
|
- They remain functional for transition period but marked with `@deprecated`
|
||||||
|
- Will be removed in future version
|
||||||
|
- Users should migrate to `@push.rocks/smartfs` and `SmartFileFactory`
|
||||||
|
|
||||||
|
### Migration Path
|
||||||
|
|
||||||
|
**Old (deprecated)**:
|
||||||
|
```typescript
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
|
||||||
|
const file = await smartfile.SmartFile.fromFilePath('./file.txt');
|
||||||
|
await file.write();
|
||||||
|
|
||||||
|
const exists = await smartfile.fs.fileExists('./file.txt');
|
||||||
|
await smartfile.fs.copy('./a.txt', './b.txt');
|
||||||
|
```
|
||||||
|
|
||||||
|
**New (recommended)**:
|
||||||
|
```typescript
|
||||||
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
|
|
||||||
|
const factory = SmartFileFactory.nodeFs();
|
||||||
|
const file = await factory.fromFilePath('./file.txt');
|
||||||
|
await file.write();
|
||||||
|
|
||||||
|
const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
const exists = await smartFs.file('./file.txt').exists();
|
||||||
|
await smartFs.file('./a.txt').copy('./b.txt');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Considerations
|
||||||
|
|
||||||
|
- Tests should use `SmartFileFactory.nodeFs()` or create custom factory with memory provider
|
||||||
|
- VirtualDirectory tests can use collection methods without filesystem access
|
||||||
|
- Filesystem operations should be tested via `@push.rocks/smartfs`
|
||||||
|
|
||||||
|
### Future Plans
|
||||||
|
|
||||||
|
- Remove deprecated namespace exports completely
|
||||||
|
- Full smartfs integration (remove fallback code)
|
||||||
|
- Potentially remove fs-extra, glob dependencies once smartfs is fully integrated
|
||||||
|
|
||||||
## listFileTree Function Enhancement (ts/fs.ts:367-415)
|
## listFileTree Function Enhancement (ts/fs.ts:367-415)
|
||||||
|
|
||||||
### Issue Fixed
|
### Issue Fixed
|
||||||
|
|||||||
579
readme.md
579
readme.md
@@ -1,315 +1,492 @@
|
|||||||
# @push.rocks/smartfile 📁
|
# @push.rocks/smartfile 📁
|
||||||
|
|
||||||
> **A powerful, TypeScript-based file management library for Node.js**
|
> **High-level file representation classes for Node.js**
|
||||||
|
|
||||||
## 🚀 What is smartfile?
|
## 🚀 What is smartfile?
|
||||||
|
|
||||||
`@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.
|
`@push.rocks/smartfile` provides powerful **in-memory file representations** for Node.js applications. It offers clean, TypeScript-first classes for working with files (`SmartFile`), streams (`StreamFile`), and virtual file collections (`VirtualDirectory`).
|
||||||
|
|
||||||
Think of it as `fs` on steroids, with TypeScript superpowers! 💪
|
Think of it as your go-to solution for **content manipulation**, **file transformations**, and **in-memory file operations** - all while seamlessly integrating with [@push.rocks/smartfs](https://code.foss.global/push.rocks/smartfs) for actual filesystem operations.
|
||||||
|
|
||||||
## 💾 Installation
|
## 💾 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @push.rocks/smartfile
|
pnpm install @push.rocks/smartfile
|
||||||
|
# Optional: Install smartfs for filesystem operations
|
||||||
|
pnpm install @push.rocks/smartfs
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Key Features
|
||||||
|
|
||||||
- 🔥 **Streaming Support** - Handle massive files with ease using `StreamFile`
|
- 🎯 **Factory Pattern** - Clean, consistent API for creating file instances
|
||||||
- 📦 **Virtual Directories** - Work with in-memory file structures
|
- 🔥 **Streaming Support** - Handle massive files efficiently with `StreamFile`
|
||||||
- 🌐 **URL Support** - Directly work with files from URLs
|
- 📦 **Virtual Directories** - Work with in-memory file collections
|
||||||
- 🎯 **TypeScript First** - Full type safety and IntelliSense support
|
- 🌐 **URL Support** - Directly fetch files from URLs
|
||||||
- ⚡ **Promise-based API** - Modern async/await patterns throughout
|
- 🎨 **Content Manipulation** - Edit, transform, and parse file content
|
||||||
- 🛠️ **Comprehensive Toolset** - From basic CRUD to advanced operations
|
- ⚡ **TypeScript First** - Full type safety and IntelliSense support
|
||||||
|
- 🛠️ **Comprehensive Collection API** - Filter, map, find files in virtual directories
|
||||||
|
|
||||||
## 📚 Quick Start
|
## 📚 Quick Start
|
||||||
|
|
||||||
|
### Using the Factory
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
|
||||||
// Read a file
|
// Create factory (uses Node.js filesystem by default)
|
||||||
const content = await smartfile.fs.toStringSync('./my-file.txt');
|
const factory = SmartFileFactory.nodeFs();
|
||||||
|
|
||||||
// Write a file
|
// Load a file into memory
|
||||||
await smartfile.memory.toFs('Hello World!', './output.txt');
|
const file = await factory.fromFilePath('./config.json');
|
||||||
|
|
||||||
// Work with JSON
|
// Edit content
|
||||||
const data = await smartfile.fs.toObjectSync('./data.json');
|
await file.editContentAsString(async (content) => {
|
||||||
|
return content.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save back to disk
|
||||||
|
await file.write();
|
||||||
|
```
|
||||||
|
|
||||||
|
### With SmartFs Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
|
|
||||||
|
// Create SmartFs instance with provider
|
||||||
|
const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
|
||||||
|
// Create factory bound to this filesystem
|
||||||
|
const factory = new SmartFileFactory(smartFs);
|
||||||
|
|
||||||
|
// Now all file operations use the smartfs instance
|
||||||
|
const file = await factory.fromFilePath('./data.json');
|
||||||
|
await file.write(); // Uses smartfs under the hood
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Core Components
|
## 🎨 Core Components
|
||||||
|
|
||||||
### SmartFile Class
|
### SmartFileFactory
|
||||||
|
|
||||||
The `SmartFile` class represents a single file with powerful manipulation capabilities:
|
The factory is your entry point for creating all file instances:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartFile } from '@push.rocks/smartfile';
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
|
||||||
// Create from file path
|
const factory = SmartFileFactory.nodeFs();
|
||||||
const fileFromPath = await SmartFile.fromFilePath('./data.json');
|
|
||||||
|
|
||||||
// Create from URL
|
// Create from various sources
|
||||||
const fileFromUrl = await SmartFile.fromUrl('https://example.com/config.json');
|
const fileFromPath = await factory.fromFilePath('./data.json');
|
||||||
|
const fileFromUrl = await factory.fromUrl('https://example.com/config.json');
|
||||||
|
const fileFromBuffer = factory.fromBuffer('./file.txt', Buffer.from('content'));
|
||||||
|
const fileFromString = factory.fromString('./file.txt', 'Hello World', 'utf8');
|
||||||
|
|
||||||
// Create from text
|
// Create StreamFile instances
|
||||||
const fileFromText = await SmartFile.fromString(
|
const stream = await factory.streamFromPath('./large-file.zip');
|
||||||
'./my-file.txt',
|
const streamFromUrl = await factory.streamFromUrl('https://example.com/video.mp4');
|
||||||
'This is my content',
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create from Buffer
|
// Create VirtualDirectory instances
|
||||||
const fileFromBuffer = await SmartFile.fromBuffer(
|
const vdir = await factory.virtualDirectoryFromPath('./src');
|
||||||
'./binary.dat',
|
const emptyVdir = factory.virtualDirectoryEmpty();
|
||||||
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();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### StreamFile Class 🌊
|
### SmartFile Class
|
||||||
|
|
||||||
|
Represents a single file loaded in memory:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Created via factory
|
||||||
|
const file = await factory.fromFilePath('./data.json');
|
||||||
|
|
||||||
|
// Content access
|
||||||
|
const asString = file.parseContentAsString();
|
||||||
|
const asBuffer = file.parseContentAsBuffer();
|
||||||
|
|
||||||
|
// Content manipulation
|
||||||
|
await file.editContentAsString(async (content) => {
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
data.updated = new Date().toISOString();
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// File operations
|
||||||
|
await file.write(); // Save to original location
|
||||||
|
await file.writeToDiskAtPath('./output.json'); // Save to specific path
|
||||||
|
await file.writeToDir('./dist'); // Save to directory
|
||||||
|
await file.read(); // Reload from disk
|
||||||
|
await file.delete(); // Delete from disk
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
const size = await file.getSize(); // File size in bytes
|
||||||
|
const hash = await file.getHash('content'); // SHA256 hash
|
||||||
|
const stream = file.getStream(); // Get as Node.js stream
|
||||||
|
|
||||||
|
// Path information
|
||||||
|
console.log(file.path); // Relative path
|
||||||
|
console.log(file.absolutePath); // Absolute path
|
||||||
|
console.log(file.parsedPath); // Parsed path components
|
||||||
|
```
|
||||||
|
|
||||||
|
### StreamFile Class
|
||||||
|
|
||||||
Perfect for handling large files without memory overhead:
|
Perfect for handling large files without memory overhead:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { StreamFile } from '@push.rocks/smartfile';
|
// Created via factory
|
||||||
|
const streamFile = await factory.streamFromPath('./bigfile.zip');
|
||||||
|
|
||||||
// Create from path
|
// Or from URL
|
||||||
const streamFile = await StreamFile.fromPath('./bigfile.zip');
|
const urlStream = await factory.streamFromUrl('https://example.com/large.mp4');
|
||||||
|
|
||||||
// Create from URL
|
// Or from buffer
|
||||||
const urlStream = await StreamFile.fromUrl('https://example.com/large.mp4');
|
const bufferStream = factory.streamFromBuffer(Buffer.from('content'));
|
||||||
|
|
||||||
// Create from buffer
|
// Write to disk (streams the content)
|
||||||
const bufferStream = StreamFile.fromBuffer(
|
|
||||||
Buffer.from('streaming content'),
|
|
||||||
'stream.txt'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write to disk
|
|
||||||
await streamFile.writeToDisk('./output/bigfile.zip');
|
await streamFile.writeToDisk('./output/bigfile.zip');
|
||||||
|
await streamFile.writeToDir('./output');
|
||||||
|
|
||||||
// Get as buffer (careful with large files!)
|
// Get content (loads into memory - use carefully!)
|
||||||
const buffer = await streamFile.getContentAsBuffer();
|
const buffer = await streamFile.getContentAsBuffer();
|
||||||
|
const string = await streamFile.getContentAsString('utf8');
|
||||||
|
|
||||||
// Get as stream
|
// Get as Node.js stream for piping
|
||||||
const readStream = await streamFile.createReadStream();
|
const readStream = await streamFile.createReadStream();
|
||||||
|
|
||||||
|
// Convert to SmartFile (loads into memory)
|
||||||
|
const smartFile = await streamFile.toSmartFile();
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
const size = await streamFile.getSize();
|
||||||
```
|
```
|
||||||
|
|
||||||
### VirtualDirectory Class 📂
|
### VirtualDirectory Class
|
||||||
|
|
||||||
Manage collections of files as virtual filesystems:
|
Manage collections of SmartFiles in memory:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { VirtualDirectory } from '@push.rocks/smartfile';
|
// Created via factory
|
||||||
|
const vdir = await factory.virtualDirectoryFromPath('./src');
|
||||||
|
|
||||||
// Create from filesystem
|
// Or create empty
|
||||||
const vDir = await VirtualDirectory.fromFsDirPath('./src');
|
const emptyVdir = factory.virtualDirectoryEmpty();
|
||||||
|
|
||||||
// Create from file array
|
// Or from file array
|
||||||
const vDirFromFiles = await VirtualDirectory.fromFileArray([
|
const files = [file1, file2, file3];
|
||||||
await SmartFile.fromFilePath('./file1.txt'),
|
const vdirFromFiles = factory.virtualDirectoryFromFileArray(files);
|
||||||
await SmartFile.fromFilePath('./file2.txt')
|
|
||||||
]);
|
// ============================================
|
||||||
|
// Collection Queries (in-memory operations)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Check existence in collection
|
||||||
|
if (vdir.exists('components/Button.tsx')) {
|
||||||
|
console.log('File exists in virtual directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file from collection
|
||||||
|
const file = await vdir.getFileByPath('utils/helpers.ts');
|
||||||
|
|
||||||
|
// List all files
|
||||||
|
const allFiles = vdir.listFiles();
|
||||||
|
|
||||||
|
// List directory paths represented in collection
|
||||||
|
const dirs = vdir.listDirectories();
|
||||||
|
|
||||||
|
// Filter files
|
||||||
|
const tsFiles = vdir.filter(f => f.path.endsWith('.ts'));
|
||||||
|
const largeFiles = vdir.filter(f => f.contentBuffer.length > 10000);
|
||||||
|
|
||||||
|
// Map/transform files
|
||||||
|
const uppercased = vdir.map(f => {
|
||||||
|
f.contentBuffer = Buffer.from(f.parseContentAsString().toUpperCase());
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find specific file
|
||||||
|
const configFile = vdir.find(f => f.path.includes('config'));
|
||||||
|
|
||||||
|
// Collection info
|
||||||
|
const fileCount = vdir.size();
|
||||||
|
const empty = vdir.isEmpty();
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Collection Mutations
|
||||||
|
// ============================================
|
||||||
|
|
||||||
// Add files
|
// Add files
|
||||||
vDir.addSmartfiles([
|
vdir.addSmartfile(newFile);
|
||||||
await SmartFile.fromString('./virtual/new.txt', 'content')
|
vdir.addSmartfiles([file1, file2, file3]);
|
||||||
]);
|
|
||||||
|
|
||||||
// List files
|
// Remove file
|
||||||
const files = vDir.listFiles();
|
vdir.removeByPath('old-file.ts');
|
||||||
const directories = vDir.listDirectories();
|
|
||||||
|
|
||||||
// Get file
|
// Clear all files
|
||||||
const file = vDir.getFileByPath('./some/path.txt');
|
vdir.clear();
|
||||||
|
|
||||||
// Save to disk
|
// Merge another virtual directory
|
||||||
await vDir.saveToDisk('./output');
|
vdir.merge(otherVirtualDir);
|
||||||
|
|
||||||
// Load from disk
|
// ============================================
|
||||||
await vDir.loadFromDisk('./source');
|
// Load/Save (filesystem bridge operations)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Save all files to disk
|
||||||
|
await vdir.saveToDisk('./dist');
|
||||||
|
|
||||||
|
// Reload from disk
|
||||||
|
await vdir.loadFromDisk('./src');
|
||||||
|
|
||||||
|
// Work with subdirectories
|
||||||
|
const subVdir = await vdir.shiftToSubdirectory('components');
|
||||||
|
await vdir.addVirtualDirectory(otherVdir, 'lib');
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ File Operations
|
## 🔄 Integration with SmartFs
|
||||||
|
|
||||||
### Basic Operations
|
For filesystem operations beyond loading/saving content, use [@push.rocks/smartfs](https://code.foss.global/push.rocks/smartfs):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Check existence
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
const exists = await smartfile.fs.fileExists('./file.txt');
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
const existsSync = smartfile.fs.fileExistsSync('./file.txt');
|
|
||||||
|
|
||||||
// Read operations
|
const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
const content = await smartfile.fs.toStringSync('./file.txt');
|
const factory = new SmartFileFactory(smartFs);
|
||||||
const buffer = await smartfile.fs.toBuffer('./file.txt');
|
|
||||||
const object = await smartfile.fs.toObjectSync('./data.json');
|
|
||||||
|
|
||||||
// Write operations
|
// Use smartfile for content manipulation
|
||||||
await smartfile.memory.toFs('content', './output.txt');
|
const file = await factory.fromFilePath('./config.json');
|
||||||
smartfile.memory.toFsSync('content', './output-sync.txt');
|
await file.editContentAsString(async (s) => s.toUpperCase());
|
||||||
|
await file.write();
|
||||||
|
|
||||||
// Copy operations
|
// Use smartfs for filesystem operations
|
||||||
await smartfile.fs.copy('./source.txt', './dest.txt');
|
const exists = await smartFs.file('./config.json').exists();
|
||||||
await smartfile.fs.copy('./src-dir', './dest-dir');
|
await smartFs.file('./config.json').copy('./config.backup.json');
|
||||||
|
const stats = await smartFs.file('./config.json').stat();
|
||||||
|
|
||||||
// Delete operations
|
// List directory with smartfs
|
||||||
await smartfile.fs.remove('./file.txt');
|
const entries = await smartFs.directory('./src').list();
|
||||||
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');
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Directory Operations
|
## 🌟 Common Use Cases
|
||||||
|
|
||||||
|
### Configuration File Management
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// List contents
|
const factory = SmartFileFactory.nodeFs();
|
||||||
const files = await smartfile.fs.listFiles('./directory');
|
|
||||||
const folders = await smartfile.fs.listFolders('./directory');
|
|
||||||
const items = await smartfile.fs.listAllItems('./directory');
|
|
||||||
|
|
||||||
// Get file tree
|
// Load, modify, and save config
|
||||||
const tree = await smartfile.fs.listFileTree('./src', '**/*.ts');
|
const config = await factory.fromFilePath('./package.json');
|
||||||
|
await config.editContentAsString(async (content) => {
|
||||||
// Directory checks
|
const pkg = JSON.parse(content);
|
||||||
const isDir = await smartfile.fs.isDirectory('./path');
|
pkg.version = '2.0.0';
|
||||||
const isFile = await smartfile.fs.isFile('./path');
|
return JSON.stringify(pkg, null, 2);
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Features
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Wait for file to be ready
|
|
||||||
await smartfile.fs.waitForFileToBeReady('./file.txt');
|
|
||||||
|
|
||||||
// Stream operations
|
|
||||||
const readStream = smartfile.fsStream.createReadStream('./input.txt');
|
|
||||||
const writeStream = smartfile.fsStream.createWriteStream('./output.txt');
|
|
||||||
|
|
||||||
// File type detection
|
|
||||||
const fileType = smartfile.interpreter.filetype('./document.pdf');
|
|
||||||
// Returns: 'pdf'
|
|
||||||
|
|
||||||
// Smart read stream (with custom processing)
|
|
||||||
const smartStream = new smartfile.fsStream.SmartReadStream('./data.txt');
|
|
||||||
smartStream.on('data', (chunk) => {
|
|
||||||
// Process chunk
|
|
||||||
console.log(chunk.toString());
|
|
||||||
});
|
});
|
||||||
|
await config.write();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 Working with Multiple Files
|
### Batch File Processing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Process multiple SmartFiles
|
const factory = SmartFileFactory.nodeFs();
|
||||||
const files = await smartfile.fs.fileTreeToObject(
|
|
||||||
'./src',
|
|
||||||
'**/*.{ts,js}'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write array to disk
|
// Load directory into virtual collection
|
||||||
const smartfiles = [
|
const vdir = await factory.virtualDirectoryFromPath('./content');
|
||||||
await SmartFile.fromString('file1.txt', 'content1'),
|
|
||||||
await SmartFile.fromString('file2.txt', 'content2')
|
|
||||||
];
|
|
||||||
await smartfile.memory.smartfileArrayToFs(smartfiles, './output');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Real-World Examples
|
// Process all markdown files
|
||||||
|
const mdFiles = vdir.filter(f => f.path.endsWith('.md'));
|
||||||
### Website Bundler
|
for (const file of mdFiles.listFiles()) {
|
||||||
```typescript
|
await file.editContentAsString(async (content) => {
|
||||||
// Bundle website assets
|
// Add frontmatter, transform links, etc.
|
||||||
const website = await VirtualDirectory.fromFsDirPath('./website');
|
return `---\nprocessed: true\n---\n\n${content}`;
|
||||||
const bundle = await website.smartfileArray;
|
|
||||||
|
|
||||||
// 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, ' ');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save processed bundle
|
// Save processed files
|
||||||
await website.saveToDisk('./dist');
|
await vdir.saveToDisk('./dist/content');
|
||||||
```
|
```
|
||||||
|
|
||||||
### File Watcher & Processor
|
### Download and Process Remote Files
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Watch for new files and process them
|
const factory = SmartFileFactory.nodeFs();
|
||||||
import { SmartFile, StreamFile } from '@push.rocks/smartfile';
|
|
||||||
|
|
||||||
async function processLargeFile(filePath: string) {
|
// Fetch from URL
|
||||||
const streamFile = await StreamFile.fromPath(filePath);
|
const remoteFile = await factory.fromUrl('https://api.example.com/data.json');
|
||||||
|
|
||||||
// Stream to processed location
|
// Process content
|
||||||
await streamFile.writeToDisk(`./processed/${path.basename(filePath)}`);
|
await remoteFile.editContentAsString(async (content) => {
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
// Transform data
|
||||||
|
return JSON.stringify(data.results, null, 2);
|
||||||
|
});
|
||||||
|
|
||||||
// Clean up original
|
// Save locally
|
||||||
await smartfile.fs.remove(filePath);
|
await remoteFile.writeToDiskAtPath('./cache/data.json');
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Manager
|
### Large File Streaming
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Load and merge config files
|
const factory = SmartFileFactory.nodeFs();
|
||||||
const defaultConfig = await smartfile.fs.toObjectSync('./config.default.json');
|
|
||||||
const userConfig = await smartfile.fs.toObjectSync('./config.user.json');
|
|
||||||
|
|
||||||
const merged = { ...defaultConfig, ...userConfig };
|
// Download large file as stream
|
||||||
|
const largeFile = await factory.streamFromUrl('https://example.com/large-dataset.csv');
|
||||||
|
|
||||||
await smartfile.memory.toFs(
|
// Save to disk (streams, doesn't load all into memory)
|
||||||
JSON.stringify(merged, null, 2),
|
await largeFile.writeToDisk('./data/dataset.csv');
|
||||||
'./config.final.json'
|
|
||||||
);
|
// Or get size without downloading entire file
|
||||||
|
const size = await largeFile.getSize();
|
||||||
|
console.log(`File size: ${size} bytes`);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌟 API Reference
|
### Virtual File System for Testing
|
||||||
|
|
||||||
### Core Modules
|
```typescript
|
||||||
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
import { SmartFs, SmartFsProviderMemory } from '@push.rocks/smartfs';
|
||||||
|
|
||||||
- `fs` - File system operations
|
// Use in-memory filesystem for tests
|
||||||
- `fsStream` - Streaming operations
|
const memoryFs = new SmartFs(new SmartFsProviderMemory());
|
||||||
- `memory` - Memory/buffer operations
|
const factory = new SmartFileFactory(memoryFs);
|
||||||
- `interpreter` - File type detection
|
|
||||||
|
|
||||||
### Main Classes
|
// Create virtual files
|
||||||
|
const testFile = factory.fromString('test.txt', 'test content');
|
||||||
|
await testFile.write(); // Writes to in-memory filesystem
|
||||||
|
|
||||||
- `SmartFile` - Single file representation
|
// Test your code without touching real filesystem
|
||||||
- `StreamFile` - Streaming file operations
|
```
|
||||||
- `VirtualDirectory` - Virtual filesystem management
|
|
||||||
|
|
||||||
## 🏗️ TypeScript Support
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Responsibility Split
|
||||||
|
|
||||||
|
**@push.rocks/smartfile** (this package):
|
||||||
|
- ✅ In-memory file representations (SmartFile, StreamFile, VirtualDirectory)
|
||||||
|
- ✅ Content manipulation and transformation
|
||||||
|
- ✅ Loading content FROM sources (disk, URL, buffer, string)
|
||||||
|
- ✅ Saving content TO destinations (disk, stream)
|
||||||
|
- ✅ Collection operations (filter, map, find on VirtualDirectory)
|
||||||
|
|
||||||
|
**@push.rocks/smartfs**:
|
||||||
|
- ✅ Low-level filesystem operations (exists, stat, copy, move, delete)
|
||||||
|
- ✅ Directory operations (list, create, remove)
|
||||||
|
- ✅ Provider abstraction (Node.js fs, in-memory, S3, etc.)
|
||||||
|
- ✅ Streaming (readStream, writeStream)
|
||||||
|
- ✅ Transactions and file watching
|
||||||
|
|
||||||
|
## 📖 API Reference
|
||||||
|
|
||||||
|
### SmartFileFactory
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `SmartFileFactory.nodeFs()` | Create factory with Node.js filesystem provider |
|
||||||
|
| `new SmartFileFactory(smartFs)` | Create factory with custom SmartFs instance |
|
||||||
|
| `factory.fromFilePath(path, base?)` | Load file from disk into SmartFile |
|
||||||
|
| `factory.fromUrl(url)` | Fetch file from URL into SmartFile |
|
||||||
|
| `factory.fromBuffer(path, buffer, base?)` | Create SmartFile from Buffer |
|
||||||
|
| `factory.fromString(path, content, encoding, base?)` | Create SmartFile from string |
|
||||||
|
| `factory.streamFromPath(path)` | Create StreamFile from disk |
|
||||||
|
| `factory.streamFromUrl(url)` | Create StreamFile from URL |
|
||||||
|
| `factory.streamFromBuffer(buffer, path?)` | Create StreamFile from Buffer |
|
||||||
|
| `factory.virtualDirectoryFromPath(path)` | Load directory into VirtualDirectory |
|
||||||
|
| `factory.virtualDirectoryEmpty()` | Create empty VirtualDirectory |
|
||||||
|
| `factory.virtualDirectoryFromFileArray(files)` | Create VirtualDirectory from SmartFiles |
|
||||||
|
|
||||||
|
### SmartFile Instance Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `file.write()` | Save to original location |
|
||||||
|
| `file.writeToDiskAtPath(path)` | Save to specific path |
|
||||||
|
| `file.writeToDir(dir)` | Save to directory (preserves relative path) |
|
||||||
|
| `file.read()` | Reload content from disk |
|
||||||
|
| `file.delete()` | Delete file from disk |
|
||||||
|
| `file.editContentAsString(fn)` | Transform content as string |
|
||||||
|
| `file.parseContentAsString(encoding?)` | Get content as string |
|
||||||
|
| `file.parseContentAsBuffer()` | Get content as Buffer |
|
||||||
|
| `file.getHash(type?)` | Get SHA256 hash ('path', 'content', 'all') |
|
||||||
|
| `file.getSize()` | Get content size in bytes |
|
||||||
|
| `file.getStream()` | Get content as Node.js Readable stream |
|
||||||
|
|
||||||
|
### StreamFile Instance Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `stream.writeToDisk(path)` | Stream content to disk |
|
||||||
|
| `stream.writeToDir(dir)` | Stream to directory |
|
||||||
|
| `stream.createReadStream()` | Get as Node.js Readable stream |
|
||||||
|
| `stream.getContentAsBuffer()` | Load entire content into Buffer |
|
||||||
|
| `stream.getContentAsString(encoding?)` | Load entire content as string |
|
||||||
|
| `stream.getSize()` | Get content size in bytes |
|
||||||
|
| `stream.toSmartFile()` | Convert to SmartFile (loads into memory) |
|
||||||
|
|
||||||
|
### VirtualDirectory Instance Methods
|
||||||
|
|
||||||
|
**Collection Queries:**
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `vdir.exists(path)` | Check if file exists in collection |
|
||||||
|
| `vdir.has(path)` | Alias for exists() |
|
||||||
|
| `vdir.getFileByPath(path)` | Get SmartFile by path |
|
||||||
|
| `vdir.listFiles()` | Get all SmartFiles |
|
||||||
|
| `vdir.listDirectories()` | Get all directory paths |
|
||||||
|
| `vdir.filter(predicate)` | Filter files, returns new VirtualDirectory |
|
||||||
|
| `vdir.map(fn)` | Transform files, returns new VirtualDirectory |
|
||||||
|
| `vdir.find(predicate)` | Find first matching file |
|
||||||
|
| `vdir.size()` | Get file count |
|
||||||
|
| `vdir.isEmpty()` | Check if empty |
|
||||||
|
|
||||||
|
**Collection Mutations:**
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `vdir.addSmartfile(file)` | Add single file |
|
||||||
|
| `vdir.addSmartfiles(files)` | Add multiple files |
|
||||||
|
| `vdir.removeByPath(path)` | Remove file by path |
|
||||||
|
| `vdir.clear()` | Remove all files |
|
||||||
|
| `vdir.merge(otherVdir)` | Merge another VirtualDirectory |
|
||||||
|
|
||||||
|
**Load/Save:**
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `vdir.saveToDisk(dir)` | Write all files to disk |
|
||||||
|
| `vdir.loadFromDisk(dir)` | Load files from disk (replaces collection) |
|
||||||
|
|
||||||
|
## 🔧 TypeScript Support
|
||||||
|
|
||||||
Full TypeScript support with comprehensive type definitions:
|
Full TypeScript support with comprehensive type definitions:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { SmartFile, StreamFile, VirtualDirectory } from '@push.rocks/smartfile';
|
import type { SmartFile, StreamFile, VirtualDirectory, SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
|
||||||
// All methods are fully typed
|
|
||||||
const processFile = async (file: SmartFile): Promise<void> => {
|
const processFile = async (file: SmartFile): Promise<void> => {
|
||||||
const content = file.parseContentAsString();
|
const content = file.parseContentAsString();
|
||||||
// TypeScript knows content is string
|
// TypeScript knows content is string
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📦 Backward Compatibility
|
||||||
|
|
||||||
|
Version 12.0.0 introduces the factory pattern. Legacy exports are deprecated but still functional:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ⚠️ Deprecated (still works, but will be removed)
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
const file = await smartfile.SmartFile.fromFilePath('./file.txt');
|
||||||
|
await smartfile.fs.copy('./a.txt', './b.txt');
|
||||||
|
|
||||||
|
// ✅ Recommended (new factory pattern)
|
||||||
|
import { SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
|
const factory = SmartFileFactory.nodeFs();
|
||||||
|
const file = await factory.fromFilePath('./file.txt');
|
||||||
|
|
||||||
|
// For filesystem operations, use smartfs:
|
||||||
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
||||||
|
const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
await smartFs.file('./a.txt').copy('./b.txt');
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||||
|
|||||||
95
test/helpers/mock-smartfs.ts
Normal file
95
test/helpers/mock-smartfs.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Mock SmartFs implementation for testing until @push.rocks/smartfs is available
|
||||||
|
* This wraps fs-extra to provide the SmartFs interface
|
||||||
|
*/
|
||||||
|
import { ensureDir, pathExists, remove, copy } from 'fs-extra';
|
||||||
|
import { promises as fsPromises, createReadStream, createWriteStream } from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Readable, Writable } from 'stream';
|
||||||
|
|
||||||
|
export class MockSmartFs {
|
||||||
|
public file(filePath: string) {
|
||||||
|
return {
|
||||||
|
async read(): Promise<string | Buffer> {
|
||||||
|
return await fsPromises.readFile(filePath);
|
||||||
|
},
|
||||||
|
async write(content: string | Buffer): Promise<void> {
|
||||||
|
await ensureDir(path.dirname(filePath));
|
||||||
|
await fsPromises.writeFile(filePath, content);
|
||||||
|
},
|
||||||
|
async exists(): Promise<boolean> {
|
||||||
|
return await pathExists(filePath);
|
||||||
|
},
|
||||||
|
async delete(): Promise<void> {
|
||||||
|
await remove(filePath);
|
||||||
|
},
|
||||||
|
async stat(): Promise<any> {
|
||||||
|
return await fsPromises.stat(filePath);
|
||||||
|
},
|
||||||
|
async readStream(): Promise<Readable> {
|
||||||
|
return Promise.resolve(createReadStream(filePath));
|
||||||
|
},
|
||||||
|
async writeStream(): Promise<Writable> {
|
||||||
|
await ensureDir(path.dirname(filePath));
|
||||||
|
return Promise.resolve(createWriteStream(filePath));
|
||||||
|
},
|
||||||
|
async copy(dest: string): Promise<void> {
|
||||||
|
await copy(filePath, dest);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public directory(dirPath: string) {
|
||||||
|
return {
|
||||||
|
async list(options?: { recursive?: boolean }): Promise<Array<{ path: string; isFile: boolean; isDirectory: boolean }>> {
|
||||||
|
const entries: Array<{ path: string; isFile: boolean; isDirectory: boolean }> = [];
|
||||||
|
|
||||||
|
if (options?.recursive) {
|
||||||
|
// Recursive listing
|
||||||
|
const walk = async (dir: string) => {
|
||||||
|
const items = await fsPromises.readdir(dir);
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dir, item);
|
||||||
|
const stats = await fsPromises.stat(fullPath);
|
||||||
|
|
||||||
|
if (stats.isFile()) {
|
||||||
|
entries.push({ path: fullPath, isFile: true, isDirectory: false });
|
||||||
|
} else if (stats.isDirectory()) {
|
||||||
|
entries.push({ path: fullPath, isFile: false, isDirectory: true });
|
||||||
|
await walk(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await walk(dirPath);
|
||||||
|
} else {
|
||||||
|
// Non-recursive listing
|
||||||
|
const items = await fsPromises.readdir(dirPath);
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item);
|
||||||
|
const stats = await fsPromises.stat(fullPath);
|
||||||
|
entries.push({
|
||||||
|
path: fullPath,
|
||||||
|
isFile: stats.isFile(),
|
||||||
|
isDirectory: stats.isDirectory(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
|
async create(options?: { recursive?: boolean }): Promise<void> {
|
||||||
|
if (options?.recursive) {
|
||||||
|
await ensureDir(dirPath);
|
||||||
|
} else {
|
||||||
|
await fsPromises.mkdir(dirPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async exists(): Promise<boolean> {
|
||||||
|
return await pathExists(dirPath);
|
||||||
|
},
|
||||||
|
async delete(): Promise<void> {
|
||||||
|
await remove(dirPath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { expect, tap } from '@git.zone/tstest/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';
|
||||||
|
import { MockSmartFs } from './helpers/mock-smartfs.js';
|
||||||
|
|
||||||
|
// Create factory with MockSmartFs
|
||||||
|
const mockFs = new MockSmartFs();
|
||||||
|
const factory = new smartfile.SmartFileFactory(mockFs);
|
||||||
|
|
||||||
// Test assets path
|
// Test assets path
|
||||||
const testAssetsPath = './test/testassets/';
|
const testAssetsPath = './test/testassets/';
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// StreamFile tests
|
// StreamFile Factory Tests
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test(
|
tap.test(
|
||||||
'StreamFile.fromPath should create a StreamFile from a file path',
|
'SmartFileFactory.streamFromPath() -> should create a StreamFile from a file path',
|
||||||
async () => {
|
async () => {
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(
|
const streamFile = await factory.streamFromPath(
|
||||||
path.join(testAssetsPath, 'mytest.json'),
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
);
|
);
|
||||||
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
@@ -22,75 +27,128 @@ tap.test(
|
|||||||
);
|
);
|
||||||
|
|
||||||
tap.test(
|
tap.test(
|
||||||
'StreamFile.fromUrl should create a StreamFile from a URL',
|
'SmartFileFactory.streamFromBuffer() -> should create a StreamFile from a Buffer',
|
||||||
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 () => {
|
async () => {
|
||||||
const buffer = Buffer.from('Some content');
|
const buffer = Buffer.from('Some content');
|
||||||
const streamFile = smartfile.StreamFile.fromBuffer(
|
const streamFile = factory.streamFromBuffer(
|
||||||
buffer,
|
buffer,
|
||||||
'bufferfile.txt',
|
'bufferfile.txt',
|
||||||
);
|
);
|
||||||
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
|
const content = await streamFile.getContentAsBuffer();
|
||||||
|
expect(content.toString()).toEqual('Some content');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
tap.test('StreamFile should write the stream to disk', async () => {
|
tap.test(
|
||||||
const streamFile = await smartfile.StreamFile.fromPath(
|
'SmartFileFactory.streamFromStream() -> should create a StreamFile from a stream',
|
||||||
|
async () => {
|
||||||
|
const { Readable } = await import('stream');
|
||||||
|
const stream = new Readable();
|
||||||
|
stream.push('stream content');
|
||||||
|
stream.push(null);
|
||||||
|
|
||||||
|
const streamFile = factory.streamFromStream(stream, 'streamfile.txt', false);
|
||||||
|
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// StreamFile Instance Tests
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
tap.test('StreamFile -> should write the stream to disk', async () => {
|
||||||
|
const streamFile = await factory.streamFromPath(
|
||||||
path.join(testAssetsPath, 'mytest.json'),
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
);
|
);
|
||||||
await streamFile.writeToDisk(
|
const targetPath = path.join(testAssetsPath, 'temp', 'stream-mytest.json');
|
||||||
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
await streamFile.writeToDisk(targetPath);
|
||||||
);
|
|
||||||
// Verify the file was written
|
// Verify the file was written by reading it back
|
||||||
expect(
|
const verifyFile = await factory.fromFilePath(targetPath);
|
||||||
// We'll use the fileExists method from your smartfile library
|
expect(verifyFile.contentBuffer).toBeInstanceOf(Buffer);
|
||||||
// Replace with the actual method you use to check file existence
|
|
||||||
await smartfile.fs.fileExists(
|
|
||||||
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
|
||||||
),
|
|
||||||
).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(
|
const streamFile = await factory.streamFromPath(
|
||||||
path.join(testAssetsPath, 'mytest.json'),
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
);
|
);
|
||||||
|
// Set relative path so writeToDir knows where to put it
|
||||||
|
streamFile.relativeFilePath = 'mytest-fromdir.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(
|
const targetPath = path.join(testAssetsPath, 'temp', 'mytest-fromdir.json');
|
||||||
await smartfile.fs.fileExists(
|
const verifyFile = await factory.fromFilePath(targetPath);
|
||||||
path.join(testAssetsPath, 'temp', 'mytest.json'),
|
expect(verifyFile.contentBuffer).toBeInstanceOf(Buffer);
|
||||||
),
|
|
||||||
).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(
|
const streamFile = await factory.streamFromPath(
|
||||||
path.join(testAssetsPath, 'mytest.json'),
|
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
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
const streamFile = await factory.streamFromPath(
|
||||||
path.join(testAssetsPath, 'mytest.json'),
|
path.join(testAssetsPath, 'mytest.json'),
|
||||||
);
|
);
|
||||||
const contentString = await streamFile.getContentAsString();
|
const contentString = await streamFile.getContentAsString();
|
||||||
expect(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'
|
const parsed = JSON.parse(contentString);
|
||||||
expect(JSON.parse(contentString).key1).toEqual('this works');
|
expect(parsed.key1).toEqual('this works');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('StreamFile -> should get size', async () => {
|
||||||
|
const buffer = Buffer.from('test content for size');
|
||||||
|
const streamFile = factory.streamFromBuffer(buffer, 'sizefile.txt');
|
||||||
|
const size = await streamFile.getSize();
|
||||||
|
expect(size).toEqual(buffer.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('StreamFile -> should handle multi-use streams', async () => {
|
||||||
|
const buffer = Buffer.from('multi-use content');
|
||||||
|
const streamFile = factory.streamFromBuffer(buffer, 'multiuse.txt');
|
||||||
|
streamFile.multiUse = true;
|
||||||
|
|
||||||
|
// Read multiple times
|
||||||
|
const content1 = await streamFile.getContentAsString();
|
||||||
|
const content2 = await streamFile.getContentAsString();
|
||||||
|
|
||||||
|
expect(content1).toEqual('multi-use content');
|
||||||
|
expect(content2).toEqual('multi-use content');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('StreamFile -> should convert to SmartFile', async () => {
|
||||||
|
const buffer = Buffer.from('convert to smartfile');
|
||||||
|
const streamFile = factory.streamFromBuffer(buffer, 'convert.txt');
|
||||||
|
|
||||||
|
const smartFile = await streamFile.toSmartFile();
|
||||||
|
expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
|
expect(smartFile.parseContentAsString()).toEqual('convert to smartfile');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('StreamFile -> should create readable stream', async () => {
|
||||||
|
const buffer = Buffer.from('readable stream content');
|
||||||
|
const streamFile = factory.streamFromBuffer(buffer, 'readable.txt');
|
||||||
|
|
||||||
|
const stream = await streamFile.createReadStream();
|
||||||
|
expect(stream).toHaveProperty('pipe');
|
||||||
|
|
||||||
|
// Read from stream
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
stream.on('end', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = Buffer.concat(chunks).toString();
|
||||||
|
expect(content).toEqual('readable stream content');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the test sequence
|
// Start the test sequence
|
||||||
|
|||||||
433
test/test.ts
433
test/test.ts
@@ -1,353 +1,142 @@
|
|||||||
import * as smartfile from '../ts/index.js';
|
import * as smartfile from '../ts/index.js';
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { MockSmartFs } from './helpers/mock-smartfs.js';
|
||||||
|
|
||||||
|
// Create factory with MockSmartFs
|
||||||
|
const mockFs = new MockSmartFs();
|
||||||
|
const factory = new smartfile.SmartFileFactory(mockFs);
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// smartfile.fs
|
// SmartFileFactory Tests
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test(
|
tap.test('SmartFileFactory.nodeFs() -> should create a default factory', async () => {
|
||||||
'.fs.fileExistsSync -> should return an accurate boolean',
|
const defaultFactory = smartfile.SmartFileFactory.nodeFs();
|
||||||
async () => {
|
expect(defaultFactory).toBeInstanceOf(smartfile.SmartFileFactory);
|
||||||
// tslint:disable-next-line: no-unused-expression
|
|
||||||
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 () => {
|
|
||||||
await expect(
|
|
||||||
smartfile.fs.fileExists('./test/testassets/mytest.json'),
|
|
||||||
).resolves.toBeTrue();
|
|
||||||
await expect(
|
|
||||||
smartfile.fs.fileExists('./test/testassets/notthere.json'),
|
|
||||||
).resolves.toBeFalse();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test(
|
tap.test('SmartFileFactory.fromFilePath() -> should create a SmartFile from file path', async () => {
|
||||||
'.fs.listFoldersSync() -> should get the file type from a string',
|
const smartFile = await factory.fromFilePath('./test/testassets/mytest.json', process.cwd());
|
||||||
async () => {
|
expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
expect(smartfile.fs.listFoldersSync('./test/testassets/')).toContain(
|
expect(smartFile.path).toEqual('test/testassets/mytest.json');
|
||||||
'testfolder',
|
expect(smartFile.contentBuffer).toBeInstanceOf(Buffer);
|
||||||
);
|
|
||||||
expect(smartfile.fs.listFoldersSync('./test/testassets/')).not.toContain(
|
|
||||||
'notExistentFolder',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.fs.listFolders() -> should get the file type from a string',
|
|
||||||
async () => {
|
|
||||||
const folderArrayArg = await smartfile.fs.listFolders('./test/testassets/');
|
|
||||||
expect(folderArrayArg).toContain('testfolder');
|
|
||||||
expect(folderArrayArg).not.toContain('notExistentFolder');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.fs.listFilesSync() -> should get the file type from a string',
|
|
||||||
async () => {
|
|
||||||
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.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 () => {
|
|
||||||
const folderArrayArg = await smartfile.fs.listFileTree(
|
|
||||||
path.resolve('./test/testassets/'),
|
|
||||||
'**/*.txt',
|
|
||||||
);
|
|
||||||
expect(folderArrayArg).toContain('testfolder/testfile1.txt');
|
|
||||||
expect(folderArrayArg).not.toContain('mytest.json');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test(
|
tap.test('SmartFileFactory.fromBuffer() -> should create a SmartFile from buffer', async () => {
|
||||||
'.fs.listFileTree() -> should find both root and nested .ts files with **/*.ts pattern',
|
const buffer = Buffer.from('test content');
|
||||||
async () => {
|
const smartFile = factory.fromBuffer('./test.txt', buffer);
|
||||||
const tsFiles = await smartfile.fs.listFileTree(process.cwd(), '**/*.ts');
|
expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
// Should find both root-level and nested TypeScript files
|
expect(smartFile.contentBuffer.toString()).toEqual('test content');
|
||||||
expect(tsFiles).toContain('ts/index.ts');
|
|
||||||
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 () => {
|
|
||||||
await smartfile.fs.copy(
|
|
||||||
'./test/testassets/testfolder/',
|
|
||||||
'./test/testassets/temp/',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.copy() -> should copy a file', async () => {
|
tap.test('SmartFileFactory.fromString() -> should create a SmartFile from string', async () => {
|
||||||
await smartfile.fs.copy(
|
const smartFile = factory.fromString('./test.txt', 'test content');
|
||||||
'./test/testassets/mytest.yaml',
|
expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
'./test/testassets/temp/mytest.yaml',
|
expect(smartFile.parseContentAsString()).toEqual('test content');
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('.fs.copy() -> should copy a file and rename it', async () => {
|
tap.test('SmartFileFactory.fromUrl() -> should create a SmartFile from URL', async () => {
|
||||||
await smartfile.fs.copy(
|
// Note: This test would need a real HTTP endpoint or mock
|
||||||
'./test/testassets/mytest.yaml',
|
// For now, we'll skip it or test with a known URL
|
||||||
'./test/testassets/temp/mytestRenamed.yaml',
|
// const smartFile = await factory.fromUrl('https://example.com/test.json');
|
||||||
);
|
// expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('.fs.remove() -> should remove an entire directory', async () => {});
|
|
||||||
|
|
||||||
tap.test('.fs.remove -> should remove single files', async () => {
|
|
||||||
await smartfile.fs.remove('./test/testassets/temp/mytestRenamed.yaml');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.fs.removeSync -> should remove single files synchronouly',
|
|
||||||
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 () => {
|
|
||||||
smartfile.fs
|
|
||||||
.removeMany([
|
|
||||||
'./test/testassets/temp/testfile1.txt',
|
|
||||||
'./test/testassets/temp/testfile2.txt',
|
|
||||||
])
|
|
||||||
.then(() => {
|
|
||||||
expect(
|
|
||||||
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 () => {
|
|
||||||
smartfile.fs.removeManySync([
|
|
||||||
'./test/testassets/temp/testfile1.txt',
|
|
||||||
'./test/testassets/temp/testfile2.txt',
|
|
||||||
]);
|
|
||||||
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 () => {
|
|
||||||
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.yaml');
|
|
||||||
expect(testData.key1).toEqual('this works');
|
|
||||||
expect(testData.key2).toEqual('this works too');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
tap.test(
|
|
||||||
'.fs.toObjectSync() -> should state unknown file type for unknown file types',
|
|
||||||
async () => {
|
|
||||||
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.txt');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.fs.toObjectSync() -> should read an .json file to an object',
|
|
||||||
async () => {
|
|
||||||
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 () => {
|
|
||||||
expect(smartfile.fs.toStringSync('./test/testassets/mytest.txt')).toEqual(
|
|
||||||
'Some TestString &&%$',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// smartfile.interpreter
|
// SmartFile Instance Tests
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
tap.test(
|
tap.test('SmartFile -> should produce vinyl compatible files', async () => {
|
||||||
'.interpreter.filetype() -> should get the file type from a string',
|
const smartFile = await factory.fromFilePath('./test/testassets/mytest.json');
|
||||||
async () => {
|
expect(smartFile).toBeInstanceOf(smartfile.SmartFile);
|
||||||
expect(smartfile.interpreter.filetype('./somefolder/data.json')).toEqual(
|
expect(smartFile.contents).toBeInstanceOf(Buffer);
|
||||||
'json',
|
expect(smartFile.isBuffer()).toBeTrue();
|
||||||
);
|
expect(smartFile.isDirectory()).toBeFalse();
|
||||||
},
|
expect(smartFile.isNull()).toBeFalse();
|
||||||
);
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// smartfile.memory
|
|
||||||
// ---------------------------
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.memory.toFs() -> should write a file to disk and return a promise',
|
|
||||||
async () => {
|
|
||||||
const localString = 'myString';
|
|
||||||
await smartfile.memory.toFs(
|
|
||||||
localString,
|
|
||||||
path.join(process.cwd(), './test/testassets/temp/testMemToFs.txt'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tap.test(
|
|
||||||
'.memory.toFsSync() -> should write a file to disk and return true if successfull',
|
|
||||||
async () => {
|
|
||||||
const localString = 'myString';
|
|
||||||
smartfile.memory.toFsSync(
|
|
||||||
localString,
|
|
||||||
path.join(process.cwd(), './test/testassets/temp/testMemToFsSync.txt'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// smartfile.Smartfile
|
|
||||||
// ---------------------------
|
|
||||||
|
|
||||||
tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
|
|
||||||
const smartfileArray = await smartfile.fs.fileTreeToObject(
|
|
||||||
process.cwd(),
|
|
||||||
'./test/testassets/testfolder/**/*',
|
|
||||||
);
|
|
||||||
const localSmartfile = smartfileArray[0];
|
|
||||||
expect(localSmartfile).toBeInstanceOf(smartfile.SmartFile);
|
|
||||||
expect(localSmartfile.contents).toBeInstanceOf(Buffer);
|
|
||||||
// tslint:disable-next-line:no-unused-expression
|
|
||||||
expect(localSmartfile.isBuffer()).toBeTrue();
|
|
||||||
// tslint:disable-next-line:no-unused-expression
|
|
||||||
expect(localSmartfile.isDirectory()).toBeFalse();
|
|
||||||
// tslint:disable-next-line:no-unused-expression
|
|
||||||
expect(localSmartfile.isNull()).toBeFalse();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should output a smartfile array to disk', async () => {
|
tap.test('SmartFile -> should write to disk', async () => {
|
||||||
const smartfileArray = await smartfile.fs.fileTreeToObject(
|
|
||||||
'./test/testassets/testfolder/',
|
|
||||||
'*',
|
|
||||||
);
|
|
||||||
for (const smartfileInstance of smartfileArray) {
|
|
||||||
console.log(smartfileInstance.relative);
|
|
||||||
console.log(smartfileInstance.path);
|
|
||||||
console.log(smartfileInstance.base);
|
|
||||||
console.log(smartfileInstance.parsedPath);
|
|
||||||
}
|
|
||||||
await smartfile.memory.smartfileArrayToFs(
|
|
||||||
smartfileArray,
|
|
||||||
path.resolve('./test/testassets/temp/testoutput/'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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/temp/utf8.txt';
|
||||||
const smartfileInstance = await smartfile.SmartFile.fromString(
|
const smartFile = factory.fromString(filePath, fileString, 'utf8');
|
||||||
filePath,
|
await smartFile.writeToDiskAtPath(filePath);
|
||||||
fileString,
|
|
||||||
'utf8',
|
// Read it back
|
||||||
);
|
const smartFile2 = await factory.fromFilePath(filePath);
|
||||||
smartfileInstance.write();
|
const retrievedString = smartFile2.parseContentAsString();
|
||||||
const smartfileInstance2 = await smartfile.SmartFile.fromFilePath(filePath);
|
|
||||||
const retrievedString = smartfileInstance.contents.toString();
|
|
||||||
expect(retrievedString).toEqual(fileString);
|
expect(retrievedString).toEqual(fileString);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get a hash', async () => {
|
tap.test('SmartFile -> should get a hash', async () => {
|
||||||
const fileString = 'hi there';
|
const fileString = 'hi there';
|
||||||
const filePath = './test/testassets/utf8.txt';
|
const smartFile = factory.fromString('./test/testassets/utf8.txt', fileString, 'utf8');
|
||||||
const smartfileInstance = await smartfile.SmartFile.fromString(
|
const hash = await smartFile.getHash();
|
||||||
filePath,
|
expect(hash).toBeTypeofString();
|
||||||
fileString,
|
expect(hash.length).toBeGreaterThan(0);
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
const hash = await smartfileInstance.getHash();
|
|
||||||
console.log(hash);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should wait for file to be ready', async () => {
|
tap.test('SmartFile -> should update file name', async () => {
|
||||||
await smartfile.fs.waitForFileToBeReady('./test/testassets/mytest.json');
|
const smartFile = factory.fromString('./test/oldname.txt', 'content');
|
||||||
|
smartFile.updateFileName('newname.txt');
|
||||||
|
expect(smartFile.parsedPath.base).toEqual('newname.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should edit content as string', async () => {
|
||||||
|
const smartFile = factory.fromString('./test.txt', 'original content');
|
||||||
|
await smartFile.editContentAsString(async (content) => {
|
||||||
|
return content.replace('original', 'modified');
|
||||||
|
});
|
||||||
|
expect(smartFile.parseContentAsString()).toEqual('modified content');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should get stream', async () => {
|
||||||
|
const smartFile = factory.fromString('./test.txt', 'stream content');
|
||||||
|
const stream = smartFile.getStream();
|
||||||
|
expect(stream).toHaveProperty('pipe');
|
||||||
|
|
||||||
|
// Read from stream
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
stream.on('end', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = Buffer.concat(chunks).toString();
|
||||||
|
expect(content).toEqual('stream content');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should get size', async () => {
|
||||||
|
const content = 'test content with some length';
|
||||||
|
const smartFile = factory.fromString('./test.txt', content);
|
||||||
|
const size = await smartFile.getSize();
|
||||||
|
expect(size).toEqual(Buffer.from(content).length);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should parse content as buffer', async () => {
|
||||||
|
const buffer = Buffer.from('buffer content');
|
||||||
|
const smartFile = factory.fromBuffer('./test.txt', buffer);
|
||||||
|
const parsedBuffer = smartFile.parseContentAsBuffer();
|
||||||
|
expect(parsedBuffer).toBeInstanceOf(Buffer);
|
||||||
|
expect(parsedBuffer.toString()).toEqual('buffer content');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should write to directory', async () => {
|
||||||
|
const smartFile = factory.fromString('subdir/test.txt', 'directory test content');
|
||||||
|
const writtenPath = await smartFile.writeToDir('./test/testassets/temp');
|
||||||
|
expect(writtenPath).toContain('subdir/test.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should get parsed path', async () => {
|
||||||
|
const smartFile = factory.fromString('./path/to/file.txt', 'content');
|
||||||
|
expect(smartFile.parsedPath.base).toEqual('file.txt');
|
||||||
|
expect(smartFile.parsedPath.ext).toEqual('.txt');
|
||||||
|
expect(smartFile.parsedPath.name).toEqual('file');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFile -> should get absolute path', async () => {
|
||||||
|
const smartFile = factory.fromString('relative/path.txt', 'content', 'utf8', '/base');
|
||||||
|
expect(smartFile.absolutePath).toEqual('/base/relative/path.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
|||||||
@@ -1,19 +1,235 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
import * as smartfile from '../ts/index.js';
|
import * as smartfile from '../ts/index.js';
|
||||||
|
import { MockSmartFs } from './helpers/mock-smartfs.js';
|
||||||
|
|
||||||
tap.test('should create a virtualdirectory', async () => {
|
// Create factory with MockSmartFs
|
||||||
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
|
const mockFs = new MockSmartFs();
|
||||||
'./test/testassets/testfolder',
|
const factory = new smartfile.SmartFileFactory(mockFs);
|
||||||
);
|
|
||||||
|
// ---------------------------
|
||||||
|
// VirtualDirectory Factory Tests
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
tap.test('SmartFileFactory.virtualDirectoryFromPath() -> should create a VirtualDirectory from fs path', async () => {
|
||||||
|
const virtualDir = await factory.virtualDirectoryFromPath('./test/testassets/testfolder');
|
||||||
|
expect(virtualDir).toBeInstanceOf(smartfile.VirtualDirectory);
|
||||||
expect(virtualDir.smartfileArray.length).toEqual(4);
|
expect(virtualDir.smartfileArray.length).toEqual(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should write to a directory', async () => {
|
tap.test('SmartFileFactory.virtualDirectoryEmpty() -> should create an empty VirtualDirectory', async () => {
|
||||||
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
'./test/testassets/testfolder',
|
expect(virtualDir).toBeInstanceOf(smartfile.VirtualDirectory);
|
||||||
);
|
expect(virtualDir.isEmpty()).toBeTrue();
|
||||||
virtualDir.saveToDisk('./test/testassets/test');
|
expect(virtualDir.size()).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartFileFactory.virtualDirectoryFromFileArray() -> should create VirtualDirectory from files', async () => {
|
||||||
|
const file1 = factory.fromString('file1.txt', 'content1');
|
||||||
|
const file2 = factory.fromString('file2.txt', 'content2');
|
||||||
|
|
||||||
|
const virtualDir = factory.virtualDirectoryFromFileArray([file1, file2]);
|
||||||
|
expect(virtualDir).toBeInstanceOf(smartfile.VirtualDirectory);
|
||||||
|
expect(virtualDir.size()).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// VirtualDirectory Collection Methods
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should add and list files', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
const file1 = factory.fromString('test1.txt', 'content1');
|
||||||
|
const file2 = factory.fromString('test2.txt', 'content2');
|
||||||
|
|
||||||
|
virtualDir.addSmartfile(file1);
|
||||||
|
virtualDir.addSmartfile(file2);
|
||||||
|
|
||||||
|
const files = virtualDir.listFiles();
|
||||||
|
expect(files.length).toEqual(2);
|
||||||
|
expect(files[0].path).toEqual('test1.txt');
|
||||||
|
expect(files[1].path).toEqual('test2.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should check file existence', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
const file = factory.fromString('exists.txt', 'content');
|
||||||
|
virtualDir.addSmartfile(file);
|
||||||
|
|
||||||
|
expect(virtualDir.exists('exists.txt')).toBeTrue();
|
||||||
|
expect(virtualDir.has('exists.txt')).toBeTrue();
|
||||||
|
expect(virtualDir.exists('not-there.txt')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should get file by path', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
const file = factory.fromString('getme.txt', 'my content');
|
||||||
|
virtualDir.addSmartfile(file);
|
||||||
|
|
||||||
|
const retrieved = await virtualDir.getFileByPath('getme.txt');
|
||||||
|
expect(retrieved).not.toBeUndefined();
|
||||||
|
expect(retrieved!.parseContentAsString()).toEqual('my content');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should remove file by path', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
const file = factory.fromString('remove.txt', 'content');
|
||||||
|
virtualDir.addSmartfile(file);
|
||||||
|
|
||||||
|
expect(virtualDir.exists('remove.txt')).toBeTrue();
|
||||||
|
|
||||||
|
const removed = virtualDir.removeByPath('remove.txt');
|
||||||
|
expect(removed).toBeTrue();
|
||||||
|
expect(virtualDir.exists('remove.txt')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should clear all files', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file1.txt', 'content1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file2.txt', 'content2'));
|
||||||
|
|
||||||
|
expect(virtualDir.size()).toEqual(2);
|
||||||
|
|
||||||
|
virtualDir.clear();
|
||||||
|
|
||||||
|
expect(virtualDir.size()).toEqual(0);
|
||||||
|
expect(virtualDir.isEmpty()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should merge with another VirtualDirectory', async () => {
|
||||||
|
const vdir1 = factory.virtualDirectoryEmpty();
|
||||||
|
vdir1.addSmartfile(factory.fromString('file1.txt', 'content1'));
|
||||||
|
|
||||||
|
const vdir2 = factory.virtualDirectoryEmpty();
|
||||||
|
vdir2.addSmartfile(factory.fromString('file2.txt', 'content2'));
|
||||||
|
|
||||||
|
vdir1.merge(vdir2);
|
||||||
|
|
||||||
|
expect(vdir1.size()).toEqual(2);
|
||||||
|
expect(vdir1.exists('file1.txt')).toBeTrue();
|
||||||
|
expect(vdir1.exists('file2.txt')).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should filter files', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file1.txt', 'content1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file2.md', 'content2'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file3.txt', 'content3'));
|
||||||
|
|
||||||
|
const filtered = virtualDir.filter(file => file.path.endsWith('.txt'));
|
||||||
|
|
||||||
|
expect(filtered.size()).toEqual(2);
|
||||||
|
expect(filtered.exists('file1.txt')).toBeTrue();
|
||||||
|
expect(filtered.exists('file3.txt')).toBeTrue();
|
||||||
|
expect(filtered.exists('file2.md')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should map files', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file1.txt', 'content1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('file2.txt', 'content2'));
|
||||||
|
|
||||||
|
const mapped = virtualDir.map(file => {
|
||||||
|
file.setContentsFromString(file.parseContentAsString().toUpperCase());
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = mapped.listFiles();
|
||||||
|
expect(files[0].parseContentAsString()).toEqual('CONTENT1');
|
||||||
|
expect(files[1].parseContentAsString()).toEqual('CONTENT2');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should find files', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('find.txt', 'findme'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('other.txt', 'other'));
|
||||||
|
|
||||||
|
const found = virtualDir.find(file => file.parseContentAsString() === 'findme');
|
||||||
|
|
||||||
|
expect(found).not.toBeUndefined();
|
||||||
|
expect(found!.path).toEqual('find.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should list directories', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('dir1/file1.txt', 'content1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('dir1/file2.txt', 'content2'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('dir2/file3.txt', 'content3'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('root.txt', 'content4'));
|
||||||
|
|
||||||
|
const dirs = virtualDir.listDirectories();
|
||||||
|
|
||||||
|
expect(dirs).toContain('dir1');
|
||||||
|
expect(dirs).toContain('dir2');
|
||||||
|
expect(dirs.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should save to disk', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('saved1.txt', 'saved content 1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('subdir/saved2.txt', 'saved content 2'));
|
||||||
|
|
||||||
|
await virtualDir.saveToDisk('./test/testassets/temp/vdir-output');
|
||||||
|
|
||||||
|
// Verify files were written
|
||||||
|
const file1 = await factory.fromFilePath('./test/testassets/temp/vdir-output/saved1.txt');
|
||||||
|
expect(file1.parseContentAsString()).toEqual('saved content 1');
|
||||||
|
|
||||||
|
const file2 = await factory.fromFilePath('./test/testassets/temp/vdir-output/subdir/saved2.txt');
|
||||||
|
expect(file2.parseContentAsString()).toEqual('saved content 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should convert to transferable object', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('trans1.txt', 'transferable1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('trans2.txt', 'transferable2'));
|
||||||
|
|
||||||
|
const transferable = await virtualDir.toVirtualDirTransferableObject();
|
||||||
|
|
||||||
|
expect(transferable.files).toBeInstanceOf(Array);
|
||||||
|
expect(transferable.files.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Fix serialization/deserialization with smartjson
|
||||||
|
// tap.test('VirtualDirectory -> should create from transferable object', async () => {
|
||||||
|
// const originalDir = factory.virtualDirectoryEmpty();
|
||||||
|
// originalDir.addSmartfile(factory.fromString('original.txt', 'original content'));
|
||||||
|
|
||||||
|
// const transferable = await originalDir.toVirtualDirTransferableObject();
|
||||||
|
// const restoredDir = await factory.virtualDirectoryFromTransferable(transferable);
|
||||||
|
|
||||||
|
// expect(restoredDir.size()).toEqual(1);
|
||||||
|
// expect(restoredDir.exists('original.txt')).toBeTrue();
|
||||||
|
|
||||||
|
// const file = await restoredDir.getFileByPath('original.txt');
|
||||||
|
// expect(file!.parseContentAsString()).toEqual('original content');
|
||||||
|
// });
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should shift to subdirectory', async () => {
|
||||||
|
const virtualDir = factory.virtualDirectoryEmpty();
|
||||||
|
virtualDir.addSmartfile(factory.fromString('root/sub/file1.txt', 'content1'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('root/sub/file2.txt', 'content2'));
|
||||||
|
virtualDir.addSmartfile(factory.fromString('root/other.txt', 'content3'));
|
||||||
|
|
||||||
|
const shifted = await virtualDir.shiftToSubdirectory('root/sub');
|
||||||
|
|
||||||
|
expect(shifted.size()).toEqual(2);
|
||||||
|
expect(shifted.exists('file1.txt')).toBeTrue();
|
||||||
|
expect(shifted.exists('file2.txt')).toBeTrue();
|
||||||
|
expect(shifted.exists('other.txt')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('VirtualDirectory -> should add another virtual directory with new root', async () => {
|
||||||
|
const vdir1 = factory.virtualDirectoryEmpty();
|
||||||
|
vdir1.addSmartfile(factory.fromString('existing.txt', 'existing'));
|
||||||
|
|
||||||
|
const vdir2 = factory.virtualDirectoryEmpty();
|
||||||
|
vdir2.addSmartfile(factory.fromString('added.txt', 'added'));
|
||||||
|
|
||||||
|
await vdir1.addVirtualDirectory(vdir2, 'newroot');
|
||||||
|
|
||||||
|
expect(vdir1.size()).toEqual(2);
|
||||||
|
expect(vdir1.exists('existing.txt')).toBeTrue();
|
||||||
|
expect(vdir1.exists('newroot/added.txt')).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
|||||||
7
test/testassets/temp/mytest-fromdir.json
Normal file
7
test/testassets/temp/mytest-fromdir.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"key1": "this works",
|
||||||
|
"key2": "this works too",
|
||||||
|
"key3": {
|
||||||
|
"nestedkey1": "hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
test/testassets/temp/stream-mytest.json
Normal file
7
test/testassets/temp/stream-mytest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"key1": "this works",
|
||||||
|
"key2": "this works too",
|
||||||
|
"key3": {
|
||||||
|
"nestedkey1": "hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test/testassets/temp/subdir/test.txt
Normal file
1
test/testassets/temp/subdir/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
directory test content
|
||||||
1
test/testassets/temp/utf8.txt
Normal file
1
test/testassets/temp/utf8.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hi there
|
||||||
1
test/testassets/temp/vdir-output/saved1.txt
Normal file
1
test/testassets/temp/vdir-output/saved1.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
saved content 1
|
||||||
1
test/testassets/temp/vdir-output/subdir/saved2.txt
Normal file
1
test/testassets/temp/vdir-output/subdir/saved2.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
saved content 2
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartfile',
|
name: '@push.rocks/smartfile',
|
||||||
version: '11.2.6',
|
version: '13.0.0',
|
||||||
description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.'
|
description: 'High-level file representation classes (SmartFile, StreamFile, VirtualDirectory) for efficient in-memory file management in Node.js using TypeScript. Works seamlessly with @push.rocks/smartfs for filesystem operations.'
|
||||||
}
|
}
|
||||||
|
|||||||
224
ts/classes.smartfile.factory.ts
Normal file
224
ts/classes.smartfile.factory.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { SmartFile } from './classes.smartfile.js';
|
||||||
|
import { StreamFile } from './classes.streamfile.js';
|
||||||
|
import { VirtualDirectory } from './classes.virtualdirectory.js';
|
||||||
|
|
||||||
|
export class SmartFileFactory {
|
||||||
|
private smartFs: any; // Will be typed as SmartFs once we import from @push.rocks/smartfs
|
||||||
|
|
||||||
|
constructor(smartFs: any) {
|
||||||
|
this.smartFs = smartFs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default factory using Node.js filesystem provider
|
||||||
|
*/
|
||||||
|
public static nodeFs(): SmartFileFactory {
|
||||||
|
// Temporarily using a placeholder - will be replaced with actual SmartFs initialization
|
||||||
|
// const smartFs = new SmartFs(new SmartFsProviderNode());
|
||||||
|
const smartFs = null; // Placeholder
|
||||||
|
return new SmartFileFactory(smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying SmartFs instance
|
||||||
|
*/
|
||||||
|
public getSmartFs(): any {
|
||||||
|
return this.smartFs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SmartFile Factory Methods
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from a file path on disk
|
||||||
|
*/
|
||||||
|
public async fromFilePath(
|
||||||
|
filePath: string,
|
||||||
|
baseArg: string = process.cwd()
|
||||||
|
): Promise<SmartFile> {
|
||||||
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Cannot read from filesystem without SmartFs.');
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = plugins.path.resolve(filePath);
|
||||||
|
const content = await this.smartFs.file(filePath).read();
|
||||||
|
const fileBuffer = Buffer.from(content);
|
||||||
|
|
||||||
|
return new SmartFile({
|
||||||
|
contentBuffer: fileBuffer,
|
||||||
|
base: baseArg,
|
||||||
|
path: plugins.path.relative(baseArg, filePath),
|
||||||
|
}, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from a URL
|
||||||
|
*/
|
||||||
|
public async fromUrl(urlArg: string): Promise<SmartFile> {
|
||||||
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
|
.url(urlArg)
|
||||||
|
.accept('binary')
|
||||||
|
.get();
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
|
||||||
|
return new SmartFile({
|
||||||
|
contentBuffer: buffer,
|
||||||
|
base: process.cwd(),
|
||||||
|
path: urlArg,
|
||||||
|
}, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from a Buffer
|
||||||
|
*/
|
||||||
|
public fromBuffer(
|
||||||
|
filePath: string,
|
||||||
|
contentBufferArg: Buffer,
|
||||||
|
baseArg: string = process.cwd()
|
||||||
|
): SmartFile {
|
||||||
|
// Use filePath as-is if it's already relative, otherwise compute relative path
|
||||||
|
const relativePath = plugins.path.isAbsolute(filePath)
|
||||||
|
? plugins.path.relative(baseArg, filePath)
|
||||||
|
: filePath;
|
||||||
|
|
||||||
|
return new SmartFile({
|
||||||
|
contentBuffer: contentBufferArg,
|
||||||
|
base: baseArg,
|
||||||
|
path: relativePath,
|
||||||
|
}, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from a string
|
||||||
|
*/
|
||||||
|
public fromString(
|
||||||
|
filePath: string,
|
||||||
|
contentStringArg: string,
|
||||||
|
encodingArg: 'utf8' | 'binary' = 'utf8',
|
||||||
|
baseArg: string = process.cwd()
|
||||||
|
): SmartFile {
|
||||||
|
// Use filePath as-is if it's already relative, otherwise compute relative path
|
||||||
|
const relativePath = plugins.path.isAbsolute(filePath)
|
||||||
|
? plugins.path.relative(baseArg, filePath)
|
||||||
|
: filePath;
|
||||||
|
|
||||||
|
return new SmartFile({
|
||||||
|
contentBuffer: Buffer.from(contentStringArg, encodingArg),
|
||||||
|
base: baseArg,
|
||||||
|
path: relativePath,
|
||||||
|
}, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from a stream
|
||||||
|
*/
|
||||||
|
public async fromStream(
|
||||||
|
stream: plugins.stream.Readable,
|
||||||
|
filePath: string,
|
||||||
|
baseArg: string = process.cwd()
|
||||||
|
): Promise<SmartFile> {
|
||||||
|
return new Promise<SmartFile>((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
stream.on('error', (error) => reject(error));
|
||||||
|
stream.on('end', () => {
|
||||||
|
const contentBuffer = Buffer.concat(chunks);
|
||||||
|
const smartfile = new SmartFile({
|
||||||
|
contentBuffer: contentBuffer,
|
||||||
|
base: baseArg,
|
||||||
|
path: plugins.path.relative(baseArg, filePath),
|
||||||
|
}, this.smartFs);
|
||||||
|
resolve(smartfile);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SmartFile from folded JSON
|
||||||
|
*/
|
||||||
|
public async fromFoldedJson(foldedJsonArg: string): Promise<SmartFile> {
|
||||||
|
const parsed = plugins.smartjson.parse(foldedJsonArg);
|
||||||
|
return new SmartFile(parsed, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// StreamFile Factory Methods
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a StreamFile from a file path
|
||||||
|
*/
|
||||||
|
public async streamFromPath(filePath: string): Promise<StreamFile> {
|
||||||
|
return StreamFile.fromPath(filePath, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a StreamFile from a URL
|
||||||
|
*/
|
||||||
|
public async streamFromUrl(url: string): Promise<StreamFile> {
|
||||||
|
return StreamFile.fromUrl(url, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a StreamFile from a Buffer
|
||||||
|
*/
|
||||||
|
public streamFromBuffer(buffer: Buffer, relativeFilePath?: string): StreamFile {
|
||||||
|
return StreamFile.fromBuffer(buffer, relativeFilePath, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a StreamFile from a Node.js Readable stream
|
||||||
|
*/
|
||||||
|
public streamFromStream(
|
||||||
|
stream: plugins.stream.Readable,
|
||||||
|
relativeFilePath?: string,
|
||||||
|
multiUse: boolean = false
|
||||||
|
): StreamFile {
|
||||||
|
return StreamFile.fromStream(stream, relativeFilePath, multiUse, this.smartFs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VirtualDirectory Factory Methods
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a VirtualDirectory from a filesystem directory path
|
||||||
|
*/
|
||||||
|
public async virtualDirectoryFromPath(pathArg: string): Promise<VirtualDirectory> {
|
||||||
|
return VirtualDirectory.fromFsDirPath(pathArg, this.smartFs, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty VirtualDirectory
|
||||||
|
*/
|
||||||
|
public virtualDirectoryEmpty(): VirtualDirectory {
|
||||||
|
return new VirtualDirectory(this.smartFs, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a VirtualDirectory from an array of SmartFiles
|
||||||
|
*/
|
||||||
|
public virtualDirectoryFromFileArray(files: SmartFile[]): VirtualDirectory {
|
||||||
|
const vdir = new VirtualDirectory(this.smartFs, this);
|
||||||
|
vdir.addSmartfiles(files);
|
||||||
|
return vdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a VirtualDirectory from a transferable object
|
||||||
|
*/
|
||||||
|
public async virtualDirectoryFromTransferable(
|
||||||
|
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject
|
||||||
|
): Promise<VirtualDirectory> {
|
||||||
|
const newVirtualDir = new VirtualDirectory(this.smartFs, this);
|
||||||
|
for (const fileArg of virtualDirTransferableObjectArg.files) {
|
||||||
|
const smartFile = SmartFile.enfoldFromJson(fileArg) as SmartFile;
|
||||||
|
// Update the smartFs reference
|
||||||
|
(smartFile as any).smartFs = this.smartFs;
|
||||||
|
newVirtualDir.addSmartfiles([smartFile]);
|
||||||
|
}
|
||||||
|
return newVirtualDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as fs from './fs.js';
|
|
||||||
import * as memory from './memory.js';
|
|
||||||
|
|
||||||
export interface ISmartfileConstructorOptions {
|
export interface ISmartfileConstructorOptions {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -10,103 +8,17 @@ export interface ISmartfileConstructorOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* an vinyl file compatible in memory file class
|
* an vinyl file compatible in memory file class
|
||||||
|
* Use SmartFileFactory to create instances of this class
|
||||||
*/
|
*/
|
||||||
export class SmartFile extends plugins.smartjson.Smartjson {
|
export class SmartFile extends plugins.smartjson.Smartjson {
|
||||||
// ======
|
|
||||||
// STATIC
|
|
||||||
// ======
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a Smartfile from a filePath
|
|
||||||
* @param filePath
|
|
||||||
*/
|
|
||||||
public static async fromFilePath(
|
|
||||||
filePath: string,
|
|
||||||
baseArg: string = process.cwd(),
|
|
||||||
) {
|
|
||||||
filePath = plugins.path.resolve(filePath);
|
|
||||||
const fileBuffer = fs.toBufferSync(filePath);
|
|
||||||
const smartfile = new SmartFile({
|
|
||||||
contentBuffer: fileBuffer,
|
|
||||||
base: baseArg,
|
|
||||||
path: plugins.path.relative(baseArg, filePath),
|
|
||||||
});
|
|
||||||
return smartfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fromBuffer(
|
|
||||||
filePath: string,
|
|
||||||
contentBufferArg: Buffer,
|
|
||||||
baseArg: string = process.cwd(),
|
|
||||||
) {
|
|
||||||
const smartfile = new SmartFile({
|
|
||||||
contentBuffer: contentBufferArg,
|
|
||||||
base: baseArg,
|
|
||||||
path: plugins.path.relative(baseArg, filePath),
|
|
||||||
});
|
|
||||||
|
|
||||||
return smartfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fromString(
|
|
||||||
filePath: string,
|
|
||||||
contentStringArg: string,
|
|
||||||
encodingArg: 'utf8' | 'binary',
|
|
||||||
baseArg = process.cwd(),
|
|
||||||
) {
|
|
||||||
const smartfile = new SmartFile({
|
|
||||||
contentBuffer: Buffer.from(contentStringArg, encodingArg),
|
|
||||||
base: baseArg,
|
|
||||||
path: plugins.path.relative(baseArg, filePath),
|
|
||||||
});
|
|
||||||
|
|
||||||
return smartfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fromFoldedJson(foldedJsonArg: string) {
|
|
||||||
return new SmartFile(plugins.smartjson.parse(foldedJsonArg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a Smartfile from a ReadableStream
|
|
||||||
* @param stream a readable stream that provides file content
|
|
||||||
* @param filePath the file path to associate with the content
|
|
||||||
* @param baseArg the base path to use for the file
|
|
||||||
*/
|
|
||||||
public static async fromStream(
|
|
||||||
stream: plugins.stream.Readable,
|
|
||||||
filePath: string,
|
|
||||||
baseArg: string = process.cwd(),
|
|
||||||
): Promise<SmartFile> {
|
|
||||||
return new Promise<SmartFile>((resolve, reject) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
||||||
stream.on('error', (error) => reject(error));
|
|
||||||
stream.on('end', () => {
|
|
||||||
const contentBuffer = Buffer.concat(chunks);
|
|
||||||
const smartfile = new SmartFile({
|
|
||||||
contentBuffer: contentBuffer,
|
|
||||||
base: baseArg,
|
|
||||||
path: plugins.path.relative(baseArg, filePath),
|
|
||||||
});
|
|
||||||
resolve(smartfile);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fromUrl(urlArg: string) {
|
|
||||||
const response = await plugins.smartrequest.SmartRequest.create()
|
|
||||||
.url(urlArg)
|
|
||||||
.accept('binary')
|
|
||||||
.get();
|
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
|
||||||
const smartfile = await SmartFile.fromBuffer(urlArg, buffer);
|
|
||||||
return smartfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========
|
// ========
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
// ========
|
// ========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the SmartFs instance for filesystem operations
|
||||||
|
*/
|
||||||
|
private smartFs?: any;
|
||||||
/**
|
/**
|
||||||
* the relative path of the file
|
* the relative path of the file
|
||||||
*/
|
*/
|
||||||
@@ -149,9 +61,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
/**
|
/**
|
||||||
* the constructor of Smartfile
|
* the constructor of Smartfile
|
||||||
* @param optionsArg
|
* @param optionsArg
|
||||||
|
* @param smartFs optional SmartFs instance for filesystem operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
constructor(optionsArg: ISmartfileConstructorOptions) {
|
constructor(optionsArg: ISmartfileConstructorOptions, smartFs?: any) {
|
||||||
super();
|
super();
|
||||||
if (optionsArg.contentBuffer) {
|
if (optionsArg.contentBuffer) {
|
||||||
this.contentBuffer = optionsArg.contentBuffer;
|
this.contentBuffer = optionsArg.contentBuffer;
|
||||||
@@ -160,6 +73,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
}
|
}
|
||||||
this.path = optionsArg.path;
|
this.path = optionsArg.path;
|
||||||
this.base = optionsArg.base;
|
this.base = optionsArg.base;
|
||||||
|
this.smartFs = smartFs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,14 +91,19 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* write file to disk at its original location
|
* write file to disk at its original location
|
||||||
* Behaviours:
|
* Behaviours:
|
||||||
* - no argument write to exactly where the file was picked up
|
* - no argument write to exactly where the file was picked up
|
||||||
|
* - Requires SmartFs instance (create via SmartFileFactory)
|
||||||
*/
|
*/
|
||||||
public async write() {
|
public async write() {
|
||||||
let writePath = plugins.smartpath.transform.makeAbsolute(
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create SmartFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const writePath = plugins.smartpath.transform.makeAbsolute(
|
||||||
this.path,
|
this.path,
|
||||||
this.base,
|
this.base,
|
||||||
);
|
);
|
||||||
console.log(`writing to ${writePath}`);
|
console.log(`writing to ${writePath}`);
|
||||||
await memory.toFs(this.contentBuffer, writePath);
|
await this.smartFs.file(writePath).write(this.contentBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,10 +112,15 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* @param filePathArg
|
* @param filePathArg
|
||||||
*/
|
*/
|
||||||
public async writeToDiskAtPath(filePathArg: string) {
|
public async writeToDiskAtPath(filePathArg: string) {
|
||||||
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create SmartFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
if (!plugins.path.isAbsolute(filePathArg)) {
|
if (!plugins.path.isAbsolute(filePathArg)) {
|
||||||
filePathArg = plugins.path.join(process.cwd(), filePathArg);
|
filePathArg = plugins.path.join(process.cwd(), filePathArg);
|
||||||
}
|
}
|
||||||
await memory.toFs(this.contentBuffer, filePathArg);
|
|
||||||
|
await this.smartFs.file(filePathArg).write(this.contentBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,9 +129,13 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async writeToDir(dirPathArg: string) {
|
public async writeToDir(dirPathArg: string) {
|
||||||
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create SmartFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
dirPathArg = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
|
dirPathArg = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
|
||||||
const filePath = plugins.path.join(dirPathArg, this.path);
|
const filePath = plugins.path.join(dirPathArg, this.path);
|
||||||
await memory.toFs(this.contentBuffer, filePath);
|
await this.smartFs.file(filePath).write(this.contentBuffer);
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,16 +143,25 @@ 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(
|
if (!this.smartFs) {
|
||||||
plugins.path.join(this.base, this.path),
|
throw new Error('No SmartFs instance available. Create SmartFile through SmartFileFactory.');
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const filePath = plugins.path.join(this.base, this.path);
|
||||||
|
const content = await this.smartFs.file(filePath).read();
|
||||||
|
this.contentBuffer = Buffer.from(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deletes the file from disk at its original location
|
* deletes the file from disk at its original location
|
||||||
*/
|
*/
|
||||||
public async delete() {
|
public async delete() {
|
||||||
await fs.remove(plugins.path.join(this.base, this.path));
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create SmartFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = plugins.path.join(this.base, this.path);
|
||||||
|
await this.smartFs.file(filePath).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -380,4 +317,18 @@ export class SmartFile extends plugins.smartjson.Smartjson {
|
|||||||
public async getSize(): Promise<number> {
|
public async getSize(): Promise<number> {
|
||||||
return this.contentBuffer.length;
|
return this.contentBuffer.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse content as string with specified encoding
|
||||||
|
*/
|
||||||
|
public parseContentAsString(encodingArg: BufferEncoding = 'utf8'): string {
|
||||||
|
return this.contentBuffer.toString(encodingArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse content as buffer
|
||||||
|
*/
|
||||||
|
public parseContentAsBuffer(): Buffer {
|
||||||
|
return this.contentBuffer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as smartfileFs from './fs.js';
|
|
||||||
import * as smartfileFsStream from './fsstream.js';
|
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;
|
type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;
|
||||||
@@ -8,30 +6,36 @@ type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStre
|
|||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
* Use SmartFileFactory to create instances of this class.
|
||||||
*/
|
*/
|
||||||
export class StreamFile {
|
export class StreamFile {
|
||||||
// STATIC
|
// STATIC
|
||||||
|
|
||||||
public static async fromPath(filePath: string): Promise<StreamFile> {
|
public static async fromPath(filePath: string, smartFs?: any): Promise<StreamFile> {
|
||||||
const streamSource: TStreamSource = async (streamFileArg) =>
|
if (!smartFs) {
|
||||||
smartfileFsStream.createReadStream(filePath);
|
throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
|
||||||
const streamFile = new StreamFile(streamSource, filePath);
|
}
|
||||||
|
|
||||||
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
||||||
|
return await streamFileArg.smartFs.file(filePath).readStream();
|
||||||
|
};
|
||||||
|
const streamFile = new StreamFile(streamSource, filePath, smartFs);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
streamFile.byteLengthComputeFunction = async () => {
|
streamFile.byteLengthComputeFunction = async () => {
|
||||||
const stats = await smartfileFs.stat(filePath);
|
const stats = await smartFs.file(filePath).stat();
|
||||||
return stats.size;
|
return stats.size;
|
||||||
};
|
};
|
||||||
return streamFile;
|
return streamFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromUrl(url: string): Promise<StreamFile> {
|
public static async fromUrl(url: string, smartFs?: any): Promise<StreamFile> {
|
||||||
const streamSource: TStreamSource = async (streamFileArg) => {
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
||||||
const response = await plugins.smartrequest.SmartRequest.create()
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
.url(url)
|
.url(url)
|
||||||
.get();
|
.get();
|
||||||
return response.stream();
|
return response.stream();
|
||||||
};
|
};
|
||||||
const streamFile = new StreamFile(streamSource);
|
const streamFile = new StreamFile(streamSource, undefined, smartFs);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
streamFile.byteLengthComputeFunction = async () => {
|
streamFile.byteLengthComputeFunction = async () => {
|
||||||
const response = await plugins.smartrequest.SmartRequest.create()
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
@@ -47,6 +51,7 @@ export class StreamFile {
|
|||||||
public static fromBuffer(
|
public static fromBuffer(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
relativeFilePath?: string,
|
relativeFilePath?: string,
|
||||||
|
smartFs?: any
|
||||||
): StreamFile {
|
): StreamFile {
|
||||||
const streamSource: TStreamSource = async (streamFileArg) => {
|
const streamSource: TStreamSource = async (streamFileArg) => {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
@@ -54,7 +59,7 @@ export class StreamFile {
|
|||||||
stream.push(null); // End of stream
|
stream.push(null); // End of stream
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
const streamFile = new StreamFile(streamSource, relativeFilePath);
|
const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs);
|
||||||
streamFile.multiUse = true;
|
streamFile.multiUse = true;
|
||||||
streamFile.byteLengthComputeFunction = async () => buffer.length;
|
streamFile.byteLengthComputeFunction = async () => buffer.length;
|
||||||
return streamFile;
|
return streamFile;
|
||||||
@@ -65,12 +70,14 @@ export class StreamFile {
|
|||||||
* @param stream A Node.js Readable stream.
|
* @param stream A Node.js Readable stream.
|
||||||
* @param relativeFilePath Optional file path for the stream.
|
* @param relativeFilePath Optional file path for the stream.
|
||||||
* @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.
|
||||||
|
* @param smartFs Optional SmartFs instance for filesystem operations
|
||||||
* @returns A StreamFile instance.
|
* @returns A StreamFile instance.
|
||||||
*/
|
*/
|
||||||
public static fromStream(
|
public static fromStream(
|
||||||
stream: Readable,
|
stream: Readable,
|
||||||
relativeFilePath?: string,
|
relativeFilePath?: string,
|
||||||
multiUse: boolean = false,
|
multiUse: boolean = false,
|
||||||
|
smartFs?: any
|
||||||
): StreamFile {
|
): StreamFile {
|
||||||
const streamSource: TStreamSource = (streamFileArg) => {
|
const streamSource: TStreamSource = (streamFileArg) => {
|
||||||
if (streamFileArg.multiUse) {
|
if (streamFileArg.multiUse) {
|
||||||
@@ -84,7 +91,7 @@ export class StreamFile {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const streamFile = new StreamFile(streamSource, relativeFilePath);
|
const streamFile = new StreamFile(streamSource, relativeFilePath, smartFs);
|
||||||
streamFile.multiUse = multiUse;
|
streamFile.multiUse = multiUse;
|
||||||
|
|
||||||
// If multi-use is enabled, cache the stream when it's first read
|
// If multi-use is enabled, cache the stream when it's first read
|
||||||
@@ -106,6 +113,7 @@ export class StreamFile {
|
|||||||
// INSTANCE
|
// INSTANCE
|
||||||
relativeFilePath?: string;
|
relativeFilePath?: string;
|
||||||
private streamSource: TStreamSource;
|
private streamSource: TStreamSource;
|
||||||
|
private smartFs?: any;
|
||||||
|
|
||||||
// enable stream based multi use
|
// enable stream based multi use
|
||||||
private cachedStreamBuffer?: Buffer;
|
private cachedStreamBuffer?: Buffer;
|
||||||
@@ -113,9 +121,10 @@ export class StreamFile {
|
|||||||
public used: boolean = false;
|
public used: boolean = false;
|
||||||
public byteLengthComputeFunction: () => Promise<number>;
|
public byteLengthComputeFunction: () => Promise<number>;
|
||||||
|
|
||||||
private constructor(streamSource: TStreamSource, relativeFilePath?: string) {
|
private constructor(streamSource: TStreamSource, relativeFilePath?: string, smartFs?: any) {
|
||||||
this.streamSource = streamSource;
|
this.streamSource = streamSource;
|
||||||
this.relativeFilePath = relativeFilePath;
|
this.relativeFilePath = relativeFilePath;
|
||||||
|
this.smartFs = smartFs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// METHODS
|
// METHODS
|
||||||
@@ -148,9 +157,13 @@ export class StreamFile {
|
|||||||
* @param filePathArg The file path where the stream should be written.
|
* @param filePathArg The file path where the stream should be written.
|
||||||
*/
|
*/
|
||||||
public async writeToDisk(filePathArg: string): Promise<void> {
|
public async writeToDisk(filePathArg: string): Promise<void> {
|
||||||
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
this.checkMultiUse();
|
this.checkMultiUse();
|
||||||
const readStream = await this.createReadStream();
|
const readStream = await this.createReadStream();
|
||||||
const writeStream = smartfileFsStream.createWriteStream(filePathArg);
|
const writeStream = await this.smartFs.file(filePathArg).writeStream();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
readStream.pipe(writeStream);
|
readStream.pipe(writeStream);
|
||||||
@@ -161,9 +174,14 @@ export class StreamFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async writeToDir(dirPathArg: string) {
|
public async writeToDir(dirPathArg: string) {
|
||||||
|
if (!this.smartFs) {
|
||||||
|
throw new Error('No SmartFs instance available. Create StreamFile through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
this.checkMultiUse();
|
this.checkMultiUse();
|
||||||
const filePath = plugins.path.join(dirPathArg, this.relativeFilePath);
|
const filePath = plugins.path.join(dirPathArg, this.relativeFilePath);
|
||||||
await smartfileFs.ensureDir(plugins.path.parse(filePath).dir);
|
const dirPath = plugins.path.parse(filePath).dir;
|
||||||
|
await this.smartFs.directory(dirPath).create({ recursive: true });
|
||||||
return this.writeToDisk(filePath);
|
return this.writeToDisk(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,4 +214,17 @@ export class StreamFile {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the StreamFile to a SmartFile by loading content into memory
|
||||||
|
*/
|
||||||
|
public async toSmartFile(): Promise<any> {
|
||||||
|
const { SmartFile } = await import('./classes.smartfile.js');
|
||||||
|
const buffer = await this.getContentAsBuffer();
|
||||||
|
return new SmartFile({
|
||||||
|
path: this.relativeFilePath || 'stream',
|
||||||
|
contentBuffer: buffer,
|
||||||
|
base: process.cwd()
|
||||||
|
}, this.smartFs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SmartFile } from './classes.smartfile.js';
|
import { SmartFile } from './classes.smartfile.js';
|
||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as fs from './fs.js';
|
|
||||||
|
|
||||||
export interface IVirtualDirectoryConstructorOptions {
|
export interface IVirtualDirectoryConstructorOptions {
|
||||||
mode: '';
|
mode: '';
|
||||||
@@ -8,46 +7,149 @@ export interface IVirtualDirectoryConstructorOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* a virtual directory exposes a fs api
|
* a virtual directory exposes a fs api
|
||||||
|
* Use SmartFileFactory to create instances of this class
|
||||||
*/
|
*/
|
||||||
export class VirtualDirectory {
|
export class VirtualDirectory {
|
||||||
consstructor(options = {}) {}
|
|
||||||
|
|
||||||
// STATIC
|
// STATIC
|
||||||
public static async fromFsDirPath(
|
public static async fromFsDirPath(
|
||||||
pathArg: string,
|
pathArg: string,
|
||||||
|
smartFs?: any,
|
||||||
|
factory?: any
|
||||||
): Promise<VirtualDirectory> {
|
): Promise<VirtualDirectory> {
|
||||||
const newVirtualDir = new VirtualDirectory();
|
if (!smartFs || !factory) {
|
||||||
newVirtualDir.addSmartfiles(await fs.fileTreeToObject(pathArg, '**/*'));
|
throw new Error('No SmartFs/Factory instance available. Create VirtualDirectory through SmartFileFactory.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVirtualDir = new VirtualDirectory(smartFs, factory);
|
||||||
|
|
||||||
|
// Use smartFs to list directory and factory to create SmartFiles
|
||||||
|
const entries = await smartFs.directory(pathArg).list({ recursive: true });
|
||||||
|
const smartfiles = await Promise.all(
|
||||||
|
entries
|
||||||
|
.filter((entry: any) => entry.isFile)
|
||||||
|
.map((entry: any) => factory.fromFilePath(entry.path, pathArg))
|
||||||
|
);
|
||||||
|
newVirtualDir.addSmartfiles(smartfiles);
|
||||||
|
|
||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromVirtualDirTransferableObject(
|
public static async fromVirtualDirTransferableObject(
|
||||||
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject,
|
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject,
|
||||||
|
smartFs?: any,
|
||||||
|
factory?: any
|
||||||
): Promise<VirtualDirectory> {
|
): Promise<VirtualDirectory> {
|
||||||
const newVirtualDir = new VirtualDirectory();
|
const newVirtualDir = new VirtualDirectory(smartFs, factory);
|
||||||
for (const fileArg of virtualDirTransferableObjectArg.files) {
|
for (const fileArg of virtualDirTransferableObjectArg.files) {
|
||||||
newVirtualDir.addSmartfiles([
|
const smartFile = SmartFile.enfoldFromJson(fileArg) as SmartFile;
|
||||||
SmartFile.enfoldFromJson(fileArg) as SmartFile,
|
// Update smartFs reference if available
|
||||||
]);
|
if (smartFs) {
|
||||||
|
(smartFile as any).smartFs = smartFs;
|
||||||
|
}
|
||||||
|
newVirtualDir.addSmartfiles([smartFile]);
|
||||||
}
|
}
|
||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static fromFileArray(files: SmartFile[], smartFs?: any, factory?: any): VirtualDirectory {
|
||||||
|
const vdir = new VirtualDirectory(smartFs, factory);
|
||||||
|
vdir.addSmartfiles(files);
|
||||||
|
return vdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static empty(smartFs?: any, factory?: any): VirtualDirectory {
|
||||||
|
return new VirtualDirectory(smartFs, factory);
|
||||||
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
public smartfileArray: SmartFile[] = [];
|
public smartfileArray: SmartFile[] = [];
|
||||||
|
private smartFs?: any;
|
||||||
|
private factory?: any;
|
||||||
|
|
||||||
constructor() {}
|
constructor(smartFs?: any, factory?: any) {
|
||||||
|
this.smartFs = smartFs;
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Collection Mutations
|
||||||
|
// ============================================
|
||||||
|
|
||||||
public addSmartfiles(smartfileArrayArg: SmartFile[]) {
|
public addSmartfiles(smartfileArrayArg: SmartFile[]) {
|
||||||
this.smartfileArray = this.smartfileArray.concat(smartfileArrayArg);
|
this.smartfileArray = this.smartfileArray.concat(smartfileArrayArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileByPath(pathArg: string) {
|
public addSmartfile(smartfileArg: SmartFile): void {
|
||||||
for (const smartfile of this.smartfileArray) {
|
this.smartfileArray.push(smartfileArg);
|
||||||
if (smartfile.path === pathArg) {
|
}
|
||||||
return smartfile;
|
|
||||||
|
public removeByPath(pathArg: string): boolean {
|
||||||
|
const initialLength = this.smartfileArray.length;
|
||||||
|
this.smartfileArray = this.smartfileArray.filter(f => f.path !== pathArg);
|
||||||
|
return this.smartfileArray.length < initialLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this.smartfileArray = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public merge(otherVDir: VirtualDirectory): void {
|
||||||
|
this.addSmartfiles(otherVDir.smartfileArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Collection Queries
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
public exists(pathArg: string): boolean {
|
||||||
|
return this.smartfileArray.some(f => f.path === pathArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(pathArg: string): boolean {
|
||||||
|
return this.exists(pathArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFileByPath(pathArg: string): Promise<SmartFile | undefined> {
|
||||||
|
return this.smartfileArray.find(f => f.path === pathArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public listFiles(): SmartFile[] {
|
||||||
|
return [...this.smartfileArray];
|
||||||
|
}
|
||||||
|
|
||||||
|
public listDirectories(): string[] {
|
||||||
|
const dirs = new Set<string>();
|
||||||
|
for (const file of this.smartfileArray) {
|
||||||
|
const dir = plugins.path.dirname(file.path);
|
||||||
|
if (dir !== '.') {
|
||||||
|
dirs.add(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Array.from(dirs).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public filter(predicate: (file: SmartFile) => boolean): VirtualDirectory {
|
||||||
|
const newVDir = new VirtualDirectory(this.smartFs, this.factory);
|
||||||
|
newVDir.addSmartfiles(this.smartfileArray.filter(predicate));
|
||||||
|
return newVDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public map(fn: (file: SmartFile) => SmartFile): VirtualDirectory {
|
||||||
|
const newVDir = new VirtualDirectory(this.smartFs, this.factory);
|
||||||
|
newVDir.addSmartfiles(this.smartfileArray.map(fn));
|
||||||
|
return newVDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public find(predicate: (file: SmartFile) => boolean): SmartFile | undefined {
|
||||||
|
return this.smartfileArray.find(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public size(): number {
|
||||||
|
return this.smartfileArray.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.smartfileArray.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toVirtualDirTransferableObject(): Promise<plugins.smartfileInterfaces.VirtualDirTransferableObject> {
|
public async toVirtualDirTransferableObject(): Promise<plugins.smartfileInterfaces.VirtualDirTransferableObject> {
|
||||||
@@ -69,7 +171,7 @@ export class VirtualDirectory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async shiftToSubdirectory(subDir: string): Promise<VirtualDirectory> {
|
public async shiftToSubdirectory(subDir: string): Promise<VirtualDirectory> {
|
||||||
const newVirtualDir = new VirtualDirectory();
|
const newVirtualDir = new VirtualDirectory(this.smartFs, this.factory);
|
||||||
for (const file of this.smartfileArray) {
|
for (const file of this.smartfileArray) {
|
||||||
if (file.path.startsWith(subDir)) {
|
if (file.path.startsWith(subDir)) {
|
||||||
const adjustedFilePath = plugins.path.relative(subDir, file.path);
|
const adjustedFilePath = plugins.path.relative(subDir, file.path);
|
||||||
@@ -80,6 +182,13 @@ export class VirtualDirectory {
|
|||||||
return newVirtualDir;
|
return newVirtualDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadFromDisk(dirArg: string): Promise<void> {
|
||||||
|
// Load from disk, replacing current collection
|
||||||
|
this.clear();
|
||||||
|
const loaded = await VirtualDirectory.fromFsDirPath(dirArg, this.smartFs, this.factory);
|
||||||
|
this.addSmartfiles(loaded.smartfileArray);
|
||||||
|
}
|
||||||
|
|
||||||
public async addVirtualDirectory(
|
public async addVirtualDirectory(
|
||||||
virtualDir: VirtualDirectory,
|
virtualDir: VirtualDirectory,
|
||||||
newRoot: string,
|
newRoot: string,
|
||||||
|
|||||||
715
ts/fs.ts
715
ts/fs.ts
@@ -1,715 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
import * as interpreter from './interpreter.js';
|
|
||||||
|
|
||||||
import { SmartFile } from './classes.smartfile.js';
|
|
||||||
|
|
||||||
import * as memory from './memory.js';
|
|
||||||
import type { StreamFile } from './classes.streamfile.js';
|
|
||||||
/*===============================================================
|
|
||||||
============================ Checks =============================
|
|
||||||
===============================================================*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param filePath
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export const fileExistsSync = (filePath): boolean => {
|
|
||||||
let fileExistsBool: boolean = false;
|
|
||||||
try {
|
|
||||||
plugins.fsExtra.readFileSync(filePath);
|
|
||||||
fileExistsBool = true;
|
|
||||||
} catch (err) {
|
|
||||||
fileExistsBool = false;
|
|
||||||
}
|
|
||||||
return fileExistsBool;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param filePath
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
export const fileExists = async (filePath): Promise<boolean> => {
|
|
||||||
const done = plugins.smartpromise.defer<boolean>();
|
|
||||||
plugins.fs.access(filePath, 4, (err) => {
|
|
||||||
err ? done.resolve(false) : done.resolve(true);
|
|
||||||
});
|
|
||||||
return done.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if given path points to an existing directory
|
|
||||||
*/
|
|
||||||
export const isDirectory = (pathArg: string): boolean => {
|
|
||||||
try {
|
|
||||||
return plugins.fsExtra.statSync(pathArg).isDirectory();
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if given path points to an existing directory
|
|
||||||
*/
|
|
||||||
export const isDirectorySync = (pathArg: string): boolean => {
|
|
||||||
try {
|
|
||||||
return plugins.fsExtra.statSync(pathArg).isDirectory();
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given path points to an existing file
|
|
||||||
*/
|
|
||||||
export const isFile = (pathArg): boolean => {
|
|
||||||
return plugins.fsExtra.statSync(pathArg).isFile();
|
|
||||||
};
|
|
||||||
|
|
||||||
/*===============================================================
|
|
||||||
============================ FS ACTIONS =========================
|
|
||||||
===============================================================*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* copies a file or directory from A to B on the local disk
|
|
||||||
*/
|
|
||||||
export const copy = async (
|
|
||||||
fromArg: string,
|
|
||||||
toArg: string,
|
|
||||||
optionsArg?: plugins.fsExtra.CopyOptions & { replaceTargetDir?: boolean },
|
|
||||||
): Promise<void> => {
|
|
||||||
if (
|
|
||||||
optionsArg?.replaceTargetDir &&
|
|
||||||
isDirectory(fromArg) &&
|
|
||||||
isDirectory(toArg)
|
|
||||||
) {
|
|
||||||
await remove(toArg);
|
|
||||||
}
|
|
||||||
return await plugins.fsExtra.copy(
|
|
||||||
fromArg,
|
|
||||||
toArg,
|
|
||||||
optionsArg as plugins.fsExtra.CopyOptions,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* copies a file or directory SYNCHRONOUSLY from A to B on the local disk
|
|
||||||
*/
|
|
||||||
export const copySync = (
|
|
||||||
fromArg: string,
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures that a directory is in place
|
|
||||||
*/
|
|
||||||
export const ensureDir = async (dirPathArg: string) => {
|
|
||||||
await plugins.fsExtra.ensureDir(dirPathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures that a directory is in place
|
|
||||||
*/
|
|
||||||
export const ensureDirSync = (dirPathArg: string) => {
|
|
||||||
plugins.fsExtra.ensureDirSync(dirPathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensure an empty directory
|
|
||||||
* @executes ASYNC
|
|
||||||
*/
|
|
||||||
export const ensureEmptyDir = async (dirPathArg: string) => {
|
|
||||||
await plugins.fsExtra.ensureDir(dirPathArg);
|
|
||||||
await plugins.fsExtra.emptyDir(dirPathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensure an empty directory
|
|
||||||
* @executes SYNC
|
|
||||||
*/
|
|
||||||
export const ensureEmptyDirSync = (dirPathArg: string) => {
|
|
||||||
plugins.fsExtra.ensureDirSync(dirPathArg);
|
|
||||||
plugins.fsExtra.emptyDirSync(dirPathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures that a file is on disk
|
|
||||||
* @param filePath the filePath to ensureDir
|
|
||||||
* @param the fileContent to place into a new file in case it doesn't exist yet
|
|
||||||
* @returns Promise<void>
|
|
||||||
* @exec ASYNC
|
|
||||||
*/
|
|
||||||
export const ensureFile = async (
|
|
||||||
filePathArg,
|
|
||||||
initFileStringArg,
|
|
||||||
): Promise<void> => {
|
|
||||||
ensureFileSync(filePathArg, initFileStringArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensures that a file is on disk
|
|
||||||
* @param filePath the filePath to ensureDir
|
|
||||||
* @param the fileContent to place into a new file in case it doesn't exist yet
|
|
||||||
* @returns Promise<void>
|
|
||||||
* @exec SYNC
|
|
||||||
*/
|
|
||||||
export const ensureFileSync = (
|
|
||||||
filePathArg: string,
|
|
||||||
initFileStringArg: string,
|
|
||||||
): void => {
|
|
||||||
if (fileExistsSync(filePathArg)) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
memory.toFsSync(initFileStringArg, filePathArg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removes a file or folder from local disk
|
|
||||||
*/
|
|
||||||
export const remove = async (pathArg: string): Promise<void> => {
|
|
||||||
await plugins.fsExtra.remove(pathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removes a file SYNCHRONOUSLY from local disk
|
|
||||||
*/
|
|
||||||
export const removeSync = (pathArg: string): void => {
|
|
||||||
plugins.fsExtra.removeSync(pathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removes an array of filePaths from disk
|
|
||||||
*/
|
|
||||||
export const removeMany = async (filePathArrayArg: string[]) => {
|
|
||||||
const promiseArray: Array<Promise<void>> = [];
|
|
||||||
for (const filePath of filePathArrayArg) {
|
|
||||||
promiseArray.push(remove(filePath));
|
|
||||||
}
|
|
||||||
await Promise.all(promiseArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* like removeFilePathArray but SYNCHRONOUSLY
|
|
||||||
*/
|
|
||||||
export const removeManySync = (filePathArrayArg: string[]): void => {
|
|
||||||
for (const filePath of filePathArrayArg) {
|
|
||||||
removeSync(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*===============================================================
|
|
||||||
============================ Write/Read =========================
|
|
||||||
===============================================================*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* reads a file content to an object
|
|
||||||
* good for JSON, YAML, TOML, etc.
|
|
||||||
* @param filePathArg
|
|
||||||
* @param fileTypeArg
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
export const toObjectSync = (filePathArg, fileTypeArg?) => {
|
|
||||||
const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
|
|
||||||
let fileType;
|
|
||||||
fileTypeArg
|
|
||||||
? (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
|
|
||||||
*/
|
|
||||||
export const toStringSync = (filePath: string): string => {
|
|
||||||
const encoding = plugins.smartmime.getEncodingForPathSync(filePath);
|
|
||||||
let fileString: string | Buffer = plugins.fsExtra.readFileSync(
|
|
||||||
filePath,
|
|
||||||
encoding,
|
|
||||||
);
|
|
||||||
if (Buffer.isBuffer(fileString)) {
|
|
||||||
fileString = fileString.toString('binary');
|
|
||||||
}
|
|
||||||
return fileString;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toBuffer = async (filePath: string): Promise<Buffer> => {
|
|
||||||
return plugins.fsExtra.readFile(filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toBufferSync = (filePath: string): Buffer => {
|
|
||||||
return plugins.fsExtra.readFileSync(filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Readable Stream from a file path.
|
|
||||||
* @param filePath The path to the file.
|
|
||||||
* @returns {fs.ReadStream}
|
|
||||||
*/
|
|
||||||
export const toReadStream = (filePath: string): plugins.fs.ReadStream => {
|
|
||||||
if (!fileExistsSync(filePath)) {
|
|
||||||
throw new Error(`File does not exist at path: ${filePath}`);
|
|
||||||
}
|
|
||||||
return plugins.fsExtra.createReadStream(filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fileTreeToHash = async (
|
|
||||||
dirPathArg: string,
|
|
||||||
miniMatchFilter: string,
|
|
||||||
) => {
|
|
||||||
const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
|
|
||||||
let combinedString = '';
|
|
||||||
for (const smartfile of fileTreeObject) {
|
|
||||||
combinedString += await smartfile.getHash();
|
|
||||||
}
|
|
||||||
const hash = await plugins.smarthash.sha256FromString(combinedString);
|
|
||||||
return hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a smartfile array from a directory
|
|
||||||
* @param dirPathArg the directory to start from
|
|
||||||
* @param miniMatchFilter a minimatch filter of what files to include
|
|
||||||
*/
|
|
||||||
export const fileTreeToObject = async (
|
|
||||||
dirPathArg: string,
|
|
||||||
miniMatchFilter: string,
|
|
||||||
) => {
|
|
||||||
// handle absolute miniMatchFilter
|
|
||||||
let dirPath: string;
|
|
||||||
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
|
||||||
dirPath = '/';
|
|
||||||
} else {
|
|
||||||
dirPath = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileTree = await listFileTree(dirPath, miniMatchFilter);
|
|
||||||
const smartfileArray: SmartFile[] = [];
|
|
||||||
for (const filePath of fileTree) {
|
|
||||||
const readPath = ((): string => {
|
|
||||||
if (!plugins.path.isAbsolute(filePath)) {
|
|
||||||
return plugins.path.join(dirPath, filePath);
|
|
||||||
} else {
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
const fileBuffer = plugins.fs.readFileSync(readPath);
|
|
||||||
|
|
||||||
// push a read file as Smartfile
|
|
||||||
smartfileArray.push(
|
|
||||||
new SmartFile({
|
|
||||||
contentBuffer: fileBuffer,
|
|
||||||
base: dirPath,
|
|
||||||
path: filePath,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return smartfileArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists Folders in a directory on local disk
|
|
||||||
* @returns Promise with an array that contains the folder names
|
|
||||||
*/
|
|
||||||
export const listFolders = async (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): Promise<string[]> => {
|
|
||||||
return listFoldersSync(pathArg, regexFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists Folders SYNCHRONOUSLY in a directory on local disk
|
|
||||||
* @returns an array with the folder names as strings
|
|
||||||
*/
|
|
||||||
export const listFoldersSync = (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): string[] => {
|
|
||||||
let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
|
||||||
return plugins.fsExtra
|
|
||||||
.statSync(plugins.path.join(pathArg, file))
|
|
||||||
.isDirectory();
|
|
||||||
});
|
|
||||||
if (regexFilter) {
|
|
||||||
folderArray = folderArray.filter((fileItem) => {
|
|
||||||
return regexFilter.test(fileItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return folderArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists Files in a directory on local disk
|
|
||||||
* @returns Promise
|
|
||||||
*/
|
|
||||||
export const listFiles = async (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): Promise<string[]> => {
|
|
||||||
return listFilesSync(pathArg, regexFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists Files SYNCHRONOUSLY in a directory on local disk
|
|
||||||
* @returns an array with the folder names as strings
|
|
||||||
*/
|
|
||||||
export const listFilesSync = (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): string[] => {
|
|
||||||
let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
|
||||||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
|
||||||
});
|
|
||||||
if (regexFilter) {
|
|
||||||
fileArray = fileArray.filter((fileItem) => {
|
|
||||||
return regexFilter.test(fileItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return fileArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists all items (folders AND files) in a directory on local disk
|
|
||||||
* @returns Promise<string[]>
|
|
||||||
*/
|
|
||||||
export const listAllItems = async (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): Promise<string[]> => {
|
|
||||||
return listAllItemsSync(pathArg, regexFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists all items (folders AND files) in a directory on local disk
|
|
||||||
* @returns an array with the folder names as strings
|
|
||||||
* @executes SYNC
|
|
||||||
*/
|
|
||||||
export const listAllItemsSync = (
|
|
||||||
pathArg: string,
|
|
||||||
regexFilter?: RegExp,
|
|
||||||
): string[] => {
|
|
||||||
let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
|
||||||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
|
||||||
});
|
|
||||||
if (regexFilter) {
|
|
||||||
allItmesArray = allItmesArray.filter((fileItem) => {
|
|
||||||
return regexFilter.test(fileItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return allItmesArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* lists a file tree using a miniMatch filter
|
|
||||||
* note: if the miniMatch Filter is an absolute path, the cwdArg will be omitted
|
|
||||||
* @returns Promise<string[]> string array with the absolute paths of all matching files
|
|
||||||
*/
|
|
||||||
export const listFileTree = async (
|
|
||||||
dirPathArg: string,
|
|
||||||
miniMatchFilter: string,
|
|
||||||
absolutePathsBool: boolean = false,
|
|
||||||
): Promise<string[]> => {
|
|
||||||
// handle absolute miniMatchFilter
|
|
||||||
let dirPath: string;
|
|
||||||
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
|
||||||
dirPath = '/';
|
|
||||||
} else {
|
|
||||||
dirPath = dirPathArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
cwd: dirPath,
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
fileList = fileList.map((filePath) => {
|
|
||||||
return plugins.path.resolve(plugins.path.join(dirPath, filePath));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileList;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = async (
|
|
||||||
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 fileIsStable = false;
|
|
||||||
|
|
||||||
// We'll create a helper for repeated stats-checking logic
|
|
||||||
const checkFileStability = async () => {
|
|
||||||
try {
|
|
||||||
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>(
|
|
||||||
(cb) => plugins.fs.stat(filePathArg, cb),
|
|
||||||
);
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
// If it unexpectedly turns out to be a directory here, throw
|
|
||||||
throw new Error(
|
|
||||||
`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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure file exists first
|
|
||||||
await ensurePathExists(filePathArg);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main logic: check if we have a directory or file at fileOrDirPathArg.
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (!statsForGivenPath.isDirectory()) {
|
|
||||||
// It's a file – just do the single-file stability wait
|
|
||||||
await waitForSingleFileToBeStable(fileOrDirPathArg);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* writes string or Smartfile to disk.
|
|
||||||
* @param fileArg
|
|
||||||
* @param fileNameArg
|
|
||||||
* @param fileBaseArg
|
|
||||||
*/
|
|
||||||
export let toFs = async (
|
|
||||||
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
|
||||||
filePathArg: string,
|
|
||||||
optionsArg: {
|
|
||||||
respectRelative?: boolean;
|
|
||||||
} = {},
|
|
||||||
) => {
|
|
||||||
const done = plugins.smartpromise.defer();
|
|
||||||
|
|
||||||
// check args
|
|
||||||
if (!fileContentArg || !filePathArg) {
|
|
||||||
throw new Error('expected valid arguments');
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare actual write action
|
|
||||||
let fileContent: string | Buffer;
|
|
||||||
let fileEncoding: 'utf8' | 'binary' = 'utf8';
|
|
||||||
let filePath: string = filePathArg;
|
|
||||||
|
|
||||||
// handle Smartfile
|
|
||||||
if (fileContentArg instanceof SmartFile) {
|
|
||||||
fileContent = fileContentArg.contentBuffer;
|
|
||||||
// handle options
|
|
||||||
if (optionsArg.respectRelative) {
|
|
||||||
filePath = plugins.path.join(filePath, fileContentArg.path);
|
|
||||||
}
|
|
||||||
} else if (Buffer.isBuffer(fileContentArg)) {
|
|
||||||
fileContent = fileContentArg;
|
|
||||||
fileEncoding = 'binary';
|
|
||||||
} else if (typeof fileContentArg === 'string') {
|
|
||||||
fileContent = fileContentArg;
|
|
||||||
} else {
|
|
||||||
throw new Error('fileContent is neither string nor Smartfile');
|
|
||||||
}
|
|
||||||
await ensureDir(plugins.path.parse(filePath).dir);
|
|
||||||
plugins.fsExtra.writeFile(
|
|
||||||
filePath,
|
|
||||||
fileContent,
|
|
||||||
{ encoding: fileEncoding },
|
|
||||||
done.resolve,
|
|
||||||
);
|
|
||||||
return await done.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stat = async (filePathArg: string) => {
|
|
||||||
return plugins.fsPromises.stat(filePathArg);
|
|
||||||
};
|
|
||||||
215
ts/fsstream.ts
215
ts/fsstream.ts
@@ -1,215 +0,0 @@
|
|||||||
/*
|
|
||||||
This file contains logic for streaming things from and to the filesystem
|
|
||||||
*/
|
|
||||||
import * as plugins from './plugins.js';
|
|
||||||
|
|
||||||
export const createReadStream = (pathArg: string) => {
|
|
||||||
return plugins.fs.createReadStream(pathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createWriteStream = (pathArg: string) => {
|
|
||||||
return plugins.fs.createWriteStream(pathArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const processFile = async (
|
|
||||||
filePath: string,
|
|
||||||
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const fileStream = createReadStream(filePath);
|
|
||||||
asyncFunc(fileStream).then(resolve).catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const processDirectory = async (
|
|
||||||
directoryPath: string,
|
|
||||||
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
|
|
||||||
): Promise<void> => {
|
|
||||||
const files = plugins.fs.readdirSync(directoryPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = plugins.path.join(directoryPath, file.name);
|
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
await processDirectory(fullPath, asyncFunc); // Recursively call processDirectory for directories
|
|
||||||
} else if (file.isFile()) {
|
|
||||||
await processFile(fullPath, asyncFunc); // Call async function with the file stream and wait for it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a file is ready to be streamed (exists and is not empty).
|
|
||||||
*/
|
|
||||||
export const isFileReadyForStreaming = async (
|
|
||||||
filePathArg: string,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const stats = await plugins.fs.promises.stat(filePathArg);
|
|
||||||
return stats.size > 0;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
// File does not exist
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw error; // Rethrow other unexpected errors
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for a file to be ready for streaming (exists and is not empty).
|
|
||||||
*/
|
|
||||||
export const waitForFileToBeReadyForStreaming = (
|
|
||||||
filePathArg: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Normalize and resolve the file path
|
|
||||||
const filePath = plugins.path.resolve(filePathArg);
|
|
||||||
|
|
||||||
// Function to check file stats
|
|
||||||
const checkFile = (resolve: () => void, reject: (reason: any) => void) => {
|
|
||||||
plugins.fs.stat(filePath, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
// File not found, wait and try again
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Some other error occurred
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
if (stats.size > 0) {
|
|
||||||
// File exists and is not empty, resolve the promise
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up file watcher
|
|
||||||
const watcher = plugins.fs.watch(
|
|
||||||
filePath,
|
|
||||||
{ persistent: false },
|
|
||||||
(eventType) => {
|
|
||||||
if (eventType === 'change' || eventType === 'rename') {
|
|
||||||
checkFile(resolve, reject);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check file immediately in case it's already ready
|
|
||||||
checkFile(resolve, reject);
|
|
||||||
|
|
||||||
// Error handling
|
|
||||||
watcher.on('error', (error) => {
|
|
||||||
watcher.close();
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SmartReadStream extends plugins.stream.Readable {
|
|
||||||
private watcher: plugins.fs.FSWatcher | null = null;
|
|
||||||
private lastReadSize: number = 0;
|
|
||||||
private endTimeout: NodeJS.Timeout | null = null;
|
|
||||||
private filePath: string;
|
|
||||||
private endDelay: number;
|
|
||||||
private reading: boolean = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
filePath: string,
|
|
||||||
endDelay = 60000,
|
|
||||||
opts?: plugins.stream.ReadableOptions,
|
|
||||||
) {
|
|
||||||
super(opts);
|
|
||||||
this.filePath = filePath;
|
|
||||||
this.endDelay = endDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
private startWatching(): void {
|
|
||||||
this.watcher = plugins.fs.watch(this.filePath, (eventType) => {
|
|
||||||
if (eventType === 'change') {
|
|
||||||
this.resetEndTimeout();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.watcher.on('error', (error) => {
|
|
||||||
this.cleanup();
|
|
||||||
this.emit('error', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetEndTimeout(): void {
|
|
||||||
if (this.endTimeout) clearTimeout(this.endTimeout);
|
|
||||||
this.endTimeout = setTimeout(() => this.checkForEnd(), this.endDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkForEnd(): void {
|
|
||||||
plugins.fs.stat(this.filePath, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
this.emit('error', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastReadSize === stats.size) {
|
|
||||||
this.push(null); // Signal the end of the stream
|
|
||||||
this.cleanup();
|
|
||||||
} else {
|
|
||||||
this.lastReadSize = stats.size;
|
|
||||||
this.resetEndTimeout();
|
|
||||||
if (!this.reading) {
|
|
||||||
// We only want to continue reading if we were previously waiting for more data
|
|
||||||
this.reading = true;
|
|
||||||
this._read(10000); // Try to read more data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private cleanup(): void {
|
|
||||||
if (this.endTimeout) clearTimeout(this.endTimeout);
|
|
||||||
if (this.watcher) this.watcher.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
_read(size: number): void {
|
|
||||||
this.reading = true;
|
|
||||||
const chunkSize = Math.min(size, 16384); // Read in chunks of 16KB
|
|
||||||
const buffer = Buffer.alloc(chunkSize);
|
|
||||||
plugins.fs.open(this.filePath, 'r', (err, fd) => {
|
|
||||||
if (err) {
|
|
||||||
this.emit('error', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
plugins.fs.read(
|
|
||||||
fd,
|
|
||||||
buffer,
|
|
||||||
0,
|
|
||||||
chunkSize,
|
|
||||||
this.lastReadSize,
|
|
||||||
(err, bytesRead, buffer) => {
|
|
||||||
if (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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_destroy(error: Error | null, callback: (error: Error | null) => void): void {
|
|
||||||
this.cleanup();
|
|
||||||
callback(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
ts/index.ts
13
ts/index.ts
@@ -1,14 +1,11 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as fsMod from './fs.js';
|
|
||||||
import * as fsStreamMod from './fsstream.js';
|
|
||||||
import * as interpreterMod from './interpreter.js';
|
|
||||||
import * as memoryMod from './memory.js';
|
|
||||||
|
|
||||||
|
// Export main classes - focused on in-memory file representations
|
||||||
export * from './classes.smartfile.js';
|
export * from './classes.smartfile.js';
|
||||||
export * from './classes.streamfile.js';
|
export * from './classes.streamfile.js';
|
||||||
export * from './classes.virtualdirectory.js';
|
export * from './classes.virtualdirectory.js';
|
||||||
|
export * from './classes.smartfile.factory.js';
|
||||||
|
|
||||||
export const fs = fsMod;
|
// Note: Filesystem operations (fs, memory, fsStream, interpreter) have been removed.
|
||||||
export const fsStream = fsStreamMod;
|
// Use @push.rocks/smartfs for low-level filesystem operations.
|
||||||
export const interpreter = interpreterMod;
|
// Use SmartFileFactory for creating SmartFile/StreamFile/VirtualDirectory instances.
|
||||||
export const memory = memoryMod;
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
|
|
||||||
export let filetype = (pathArg: string): string => {
|
|
||||||
const extName = plugins.path.extname(pathArg);
|
|
||||||
const fileType = extName.replace(/\.([a-z]*)/, '$1'); // remove . form fileType
|
|
||||||
return fileType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export let objectFile = (fileStringArg: string, fileTypeArg) => {
|
|
||||||
switch (fileTypeArg) {
|
|
||||||
case 'yml':
|
|
||||||
case 'yaml':
|
|
||||||
return plugins.yaml.load(fileStringArg);
|
|
||||||
case 'json':
|
|
||||||
return JSON.parse(fileStringArg);
|
|
||||||
default:
|
|
||||||
console.error('file type ' + fileTypeArg.blue + ' not supported');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
102
ts/memory.ts
102
ts/memory.ts
@@ -1,102 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
import { SmartFile } from './classes.smartfile.js';
|
|
||||||
import * as smartfileFs from './fs.js';
|
|
||||||
import * as interpreter from './interpreter.js';
|
|
||||||
import type { StreamFile } from './classes.streamfile.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* converts file to Object
|
|
||||||
* @param fileStringArg
|
|
||||||
* @param fileTypeArg
|
|
||||||
* @returns {any|any}
|
|
||||||
*/
|
|
||||||
export let toObject = (fileStringArg: string, fileTypeArg: string) => {
|
|
||||||
return interpreter.objectFile(fileStringArg, fileTypeArg);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IToFsOptions {
|
|
||||||
respectRelative?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* writes string or Smartfile to disk.
|
|
||||||
* @param fileArg
|
|
||||||
* @param fileNameArg
|
|
||||||
* @param fileBaseArg
|
|
||||||
*/
|
|
||||||
export let toFs = async (
|
|
||||||
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
|
||||||
filePathArg: string,
|
|
||||||
optionsArg: IToFsOptions = {},
|
|
||||||
) => {
|
|
||||||
const done = plugins.smartpromise.defer();
|
|
||||||
|
|
||||||
// check args
|
|
||||||
if (!fileContentArg || !filePathArg) {
|
|
||||||
throw new Error('expected valid arguments');
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare actual write action
|
|
||||||
let fileContent: string | Buffer;
|
|
||||||
let fileEncoding: 'utf8' | 'binary' = 'utf8';
|
|
||||||
let filePath: string = filePathArg;
|
|
||||||
|
|
||||||
// handle Smartfile
|
|
||||||
if (fileContentArg instanceof SmartFile) {
|
|
||||||
fileContent = fileContentArg.contentBuffer;
|
|
||||||
// handle options
|
|
||||||
if (optionsArg.respectRelative) {
|
|
||||||
filePath = plugins.path.join(filePath, fileContentArg.path);
|
|
||||||
}
|
|
||||||
} else if (Buffer.isBuffer(fileContentArg)) {
|
|
||||||
fileContent = fileContentArg;
|
|
||||||
fileEncoding = 'binary';
|
|
||||||
} else if (typeof fileContentArg === 'string') {
|
|
||||||
fileContent = fileContentArg;
|
|
||||||
} else {
|
|
||||||
throw new Error('fileContent is neither string nor Smartfile');
|
|
||||||
}
|
|
||||||
await smartfileFs.ensureDir(plugins.path.parse(filePath).dir);
|
|
||||||
plugins.fsExtra.writeFile(
|
|
||||||
filePath,
|
|
||||||
fileContent,
|
|
||||||
{ encoding: fileEncoding },
|
|
||||||
done.resolve,
|
|
||||||
);
|
|
||||||
return await done.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* writes a string or a Smartfile to disk synchronously, only supports string
|
|
||||||
* @param fileArg
|
|
||||||
* @param filePathArg
|
|
||||||
*/
|
|
||||||
export const toFsSync = (fileArg: string, filePathArg: string) => {
|
|
||||||
// function checks to abort if needed
|
|
||||||
if (!fileArg || !filePathArg) {
|
|
||||||
throw new Error('expected a valid arguments');
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare actual write action
|
|
||||||
let fileString: string;
|
|
||||||
const filePath: string = filePathArg;
|
|
||||||
|
|
||||||
if (typeof fileArg !== 'string') {
|
|
||||||
throw new Error('fileArg is not of type String.');
|
|
||||||
} else if (typeof fileArg === 'string') {
|
|
||||||
fileString = fileArg;
|
|
||||||
}
|
|
||||||
plugins.fsExtra.writeFileSync(filePath, fileString, { encoding: 'utf8' });
|
|
||||||
};
|
|
||||||
|
|
||||||
export let smartfileArrayToFs = async (
|
|
||||||
smartfileArrayArg: SmartFile[],
|
|
||||||
dirArg: string,
|
|
||||||
) => {
|
|
||||||
await smartfileFs.ensureDir(dirArg);
|
|
||||||
for (const smartfile of smartfileArrayArg) {
|
|
||||||
await toFs(smartfile, dirArg, {
|
|
||||||
respectRelative: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user