Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rm methods #8

Merged
merged 17 commits into from
Jan 9, 2023
Merged
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