Skip to content

Commit

Permalink
Merge pull request #8 from cyclic-software/rm-methods
Browse files Browse the repository at this point in the history
Rm methods
  • Loading branch information
seekayel authored Jan 9, 2023
2 parents 80b7170 + decc004 commit 09f8d5e
Show file tree
Hide file tree
Showing 4 changed files with 464 additions and 51 deletions.
72 changes: 28 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

Drop in replacement for the Node.js `fs` library backed by AWS S3.

## Supported methods
`@cyclic.sh/s3fs` supports the following `fs` methods operating on AWS S3:
- writeFile / writeFileSync
- readFile / readFileSync
- exists / existsSync
- rm / rmSync
- stat / statSync
- unlink / unlinkSync
- readdir / readdirSync
- mkdir / mkdirSync
- rmdir / rmdirSync

## Example Usage
### Installation

```
npm install @cyclic.sh/s3fs
```


Require in the same format as Node.js `fs`, specifying an S3 Bucket:
- Callbacks and Sync methods:
```js
Expand All @@ -12,42 +32,14 @@ Require in the same format as Node.js `fs`, specifying an S3 Bucket:
const fs = require('@cyclic.sh/s3fs/promises')(S3_BUCKET_NAME)
```

## Supported methods
`@cyclic.sh/s3fs` supports the following `fs` methods operating on AWS S3:
- [x] fs.writeFile(filename, data, [options], callback)
- [x] promise
- [x] cb
- [x] sync
- [x] fs.readFile(filename, [options], callback)
- [x] promise
- [x] cb
- [x] sync
- [x] fs.exists(path, callback)
- [x] promise
- [x] cb
- [x] sync
- [x] fs.readdir(path, callback)
- [x] promise
- [x] cb
- [x] sync
- [x] fs.mkdir(path, [mode], callback)
- [x] promise
- [x] cb
- [x] sync
- [x] fs.stat(path, callback)
- [x] promise
- [x] cb
- [x] sync
- [ ] fs.rmdir(path, callback)
- [ ] fs.rm(path, callback)
- [ ] fs.unlink(path, callback)
- [ ] fs.lstat(path, callback)
- [ ] fs.createReadStream(path, [options])
- [ ] fs.createWriteStream(path, [options])

## Example Usage
### Authentication
Authenticating the client can be done with one of two ways:

Authenticating the client:
- **cyclic.sh** -
- When deploying on <a href="https://cyclic.sh" target="_blank">cyclic.sh</a>, credentials are already available in the environment
- The bucket name is also available under the `CYCLIC_BUCKET_NAME` variable
- read more: <a href="https://docs.cyclic.sh/concepts/env_vars#cyclic" target="_blank">Cyclic Environment Variables</a>
- **Local Mode** - When no credentials are available - the client will fall back to using `fs` and the local filesystem with a warning.
- **Environment Variables** - the internal S3 client will use AWS credentials if set in the environment
```
AWS_REGION
Expand All @@ -62,9 +54,6 @@ Authenticating the client can be done with one of two ways:
credentials: {...}
})
```
- **Local Mode** - When no credentials are available - the client will fall back to using `fs` and the local filesystem with a warning.


### Using Methods
The supported methods have the same API as Node.js `fs`:
- Sync
Expand All @@ -85,9 +74,4 @@ The supported methods have the same API as Node.js `fs`:
async function run(){
const json = JSON.parse(await fs.readFile('test/_read.json'))
}
```

refer to fs, s3fs:

- https://github.com/TooTallNate/s3fs
- https://nodejs.org/docs/latest-v0.10.x/api/fs.html#fs_fs_mkdir_path_mode_callback
```
142 changes: 137 additions & 5 deletions src/CyclicS3FSPromises.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ const {
PutObjectCommand,
HeadObjectCommand,
ListObjectsCommand,
ListObjectsV2Command
ListObjectsV2Command,
ListObjectVersionsCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
} = require("@aws-sdk/client-s3");
const _path = require('path')
const {Stats} = require('fs')
Expand Down Expand Up @@ -68,9 +71,10 @@ class CyclicS3FSPromises{
}

async stat(fileName, data, options={}){
fileName = util.normalize_path(fileName)
const cmd = new HeadObjectCommand({
Bucket: this.bucket,
Key: util.normalize_path(fileName)
Key: fileName
})
let result;
try{
Expand Down Expand Up @@ -98,7 +102,7 @@ class CyclicS3FSPromises{
}));
}catch(e){
if(e.name === 'NotFound'){
throw new Error(`Error: ENOENT: no such file or directory, stat '${fileName}'`)
throw new Error(`ENOENT: no such file or directory, stat '${fileName}'`)
}else{
throw e
}
Expand All @@ -121,7 +125,7 @@ class CyclicS3FSPromises{

async readdir(path){
path = util.normalize_dir(path)
const cmd = new ListObjectsCommand({
const cmd = new ListObjectsV2Command({
Bucket: this.bucket,
// StartAfter: path,
Prefix: path,
Expand All @@ -145,14 +149,142 @@ class CyclicS3FSPromises{
result = folders.concat(files).filter(r=>{return r.length})
}catch(e){
if(e.name === 'NotFound' || e.message === 'NotFound'){
throw new Error(`Error: ENOENT: no such file or directory, scandir '${path}'`)
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`)
}else{
throw e
}
}
return result
}

async rm(path){
try{
let f = await Promise.allSettled([
this.stat(path),
this.readdir(path)
])

if(f[0].status == 'rejected' && f[1].status == 'fulfilled'){
throw new Error(`SystemError [ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) ${path}`)
}
if(f[0].status == 'rejected' && f[1].status == 'rejected'){
throw f[0].reason
}

}catch(e){
throw e
}
path = util.normalize_path(path)
const cmd = new DeleteObjectCommand({
Bucket: this.bucket,
Key: path
})
try{
await this.s3.send(cmd)
}catch(e){
throw e
}

}

async rmdir(path){
try{
let contents = await this.readdir(path)
if(contents.length){
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`)
}
}catch(e){
throw e
}

path = util.normalize_dir(path)
const cmd = new DeleteObjectCommand({
Bucket: this.bucket,
Key: path
})
try{
await this.s3.send(cmd)
}catch(e){
throw e
}
}

async unlink(path){
try{
let f = await Promise.allSettled([
this.stat(path),
this.readdir(path)
])

if(f[0].status == 'rejected' && f[1].status == 'fulfilled'){
throw new Error(`EPERM: operation not permitted, unlink '${path}'`)
}
if(f[0].status == 'rejected' && f[1].status == 'rejected'){
throw f[0].reason
}

}catch(e){
throw e
}
path = util.normalize_path(path)
const cmd = new DeleteObjectCommand({
Bucket: this.bucket,
Key: path
})
try{
await this.s3.send(cmd)
}catch(e){
throw e
}
}


async deleteVersionMarkers(NextKeyMarker, list=[] ){
if (NextKeyMarker || list.length === 0) {
return await this.s3.send(new ListObjectVersionsCommand({
Bucket: this.bucket,
NextKeyMarker
})).then(async ({ DeleteMarkers, Versions, NextKeyMarker }) => {
if (DeleteMarkers && DeleteMarkers.length) {
await this.s3.send(new DeleteObjectsCommand({
Bucket: this.bucket,
Delete: {
Objects: DeleteMarkers.map((item) => ({
Key: item.Key,
VersionId: item.VersionId,
})),
},
}))

return await this.deleteVersionMarkers(NextKeyMarker, [
...list,
...DeleteMarkers.map((item) => item.Key),
]);
}

if (Versions && Versions.length) {
await this.s3.send(new DeleteObjectsCommand({
Bucket: this.bucket,
Delete: {
Objects: Versions.map((item) => ({
Key: item.Key,
VersionId: item.VersionId,
})),
},
}))
return await this.deleteVersionMarkers(NextKeyMarker, [
...list,
...Versions.map((item) => item.Key),
]);
}
return list;
});
}
return list;
};



}


Expand Down
55 changes: 54 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ function makeCallback(cb) {
if (cb === undefined) {
return rethrow();
}

if (typeof cb !== 'function') {
throw new TypeError('callback must be a function');
}
Expand Down Expand Up @@ -98,6 +97,48 @@ class CyclicS3FS extends CyclicS3FSPromises {
})
}

rm(path, callback) {
callback = makeCallback(arguments[arguments.length - 1]);
new Promise(async (resolve,reject)=>{
try{
this.stat = super.stat
this.readdir = super.readdir
let res = await super.rm(...arguments)
return resolve(callback(null,res))
}catch(e){
return resolve(callback(e))
}
})
}

unlink(path, callback) {
callback = makeCallback(arguments[arguments.length - 1]);
new Promise(async (resolve,reject)=>{
try{
this.stat = super.stat
this.readdir = super.readdir
let res = await super.unlink(...arguments)
return resolve(callback(null,res))
}catch(e){
return resolve(callback(e))
}
})
}

rmdir(path, callback) {
callback = makeCallback(arguments[arguments.length - 1]);
new Promise(async (resolve,reject)=>{
try{
this.stat = super.stat
this.readdir = super.readdir
let res = await super.rmdir(...arguments)
return resolve(callback(null,res))
}catch(e){
return resolve(callback(e))
}
})
}


readFileSync(fileName) {
return sync_interface.runSync(this,'readFile',[fileName])
Expand Down Expand Up @@ -130,6 +171,18 @@ class CyclicS3FS extends CyclicS3FSPromises {
return sync_interface.runSync(this,'mkdir',[path])
}

rmSync(path) {
return sync_interface.runSync(this,'rm',[path])
}

unlinkSync(path) {
return sync_interface.runSync(this,'unlink',[path])
}

rmdirSync(path) {
return sync_interface.runSync(this,'rmdir',[path])
}

}


Expand Down
Loading

0 comments on commit 09f8d5e

Please sign in to comment.