-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
db4ce55
commit e93eaff
Showing
7 changed files
with
262 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../.npmignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "@slangroom/fs", | ||
"version": "1.0.0", | ||
"dependencies": { | ||
"@slangroom/core": "workspace:*", | ||
"axios": "^1.5.1", | ||
"extract-zip": "^2.0.1" | ||
}, | ||
"repository": "https://github.com/dyne/slangroom", | ||
"license": "AGPL-3.0-only", | ||
"type": "module", | ||
"main": "./build/cjs/src/index.js", | ||
"types": "./build/cjs/src/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": { | ||
"types": "./build/esm/src/index.d.ts", | ||
"default": "./build/esm/src/index.js" | ||
}, | ||
"require": { | ||
"types": "./build/cjs/src/index.d.ts", | ||
"default": "./build/cjs/src/index.js" | ||
} | ||
}, | ||
"./*": { | ||
"import": { | ||
"types": "./build/esm/src/*.d.ts", | ||
"default": "./build/esm/src/*.js" | ||
}, | ||
"require": { | ||
"types": "./build/cjs/src/*.d.ts", | ||
"default": "./build/cjs/src/*.js" | ||
} | ||
} | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@slangroom/fs/plugins'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import type { PluginContext, PluginResult } from '@slangroom/core'; | ||
import * as path from 'node:path'; | ||
import * as fs from 'node:fs/promises'; | ||
import * as os from 'node:os'; | ||
import axios from 'axios'; | ||
import extractZip from 'extract-zip'; | ||
|
||
export const SandboxDir = process.env['FILES_DIR']; | ||
if (!SandboxDir) throw new Error('$FILE_DIR must be provided'); | ||
|
||
const resolveDirPath = (unsafe: string) => { | ||
const normalized = path.normalize(unsafe); | ||
// `/` and `..` prevent directory traversal | ||
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..'); | ||
// Unlike `resolveFilepath`, we allow `.` to be used here, obviously. | ||
if (doesDirectoryTraversal) return { error: `dirpath is unsafe: ${unsafe}` }; | ||
return { dirpath: path.dirname(path.join(SandboxDir, normalized)) }; | ||
}; | ||
|
||
const resolveFilepath = (unsafe: string) => { | ||
const normalized = path.normalize(unsafe); | ||
// `/` and `..` prevent directory traversal | ||
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..'); | ||
// `.` ensures that `foo/bar` or `./foo` is valid, while `.` isn't | ||
// (that is, no "real" filepath is provided) | ||
const DoesntProvideFile = normalized.startsWith('.'); | ||
if (doesDirectoryTraversal || DoesntProvideFile) | ||
return { error: `filepath is unsafe: ${unsafe}` }; | ||
return { filepath: path.join(SandboxDir, normalized) }; | ||
}; | ||
|
||
const readFile = async (safePath: string) => { | ||
const str = await fs.readFile(safePath, 'utf8'); | ||
return JSON.parse(str); | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const executeDownloadExtract = async (ctx: PluginContext): Promise<PluginResult> => { | ||
const zipUrl = ctx.fetchConnect()[0]; | ||
const unsafe = ctx.fetch('path'); | ||
if (typeof unsafe !== 'string') return ctx.fail('path must be string'); | ||
|
||
const { dirpath: dirPath, error } = resolveDirPath(unsafe); | ||
if (!dirPath) return ctx.fail(error); | ||
await fs.mkdir(dirPath, { recursive: true }); | ||
|
||
try { | ||
const resp = await axios.get(zipUrl, { responseType: 'arraybuffer' }); | ||
const tempdir = await fs.mkdtemp(path.join(os.tmpdir(), 'slangroom-')); | ||
const tempfile = path.join(tempdir, 'downloaded'); | ||
await fs.writeFile(tempfile, resp.data); | ||
await extractZip(tempfile, { dir: dirPath }); | ||
await fs.rm(tempdir, { recursive: true }); | ||
return ctx.pass('yes'); | ||
} catch (e) { | ||
if (e instanceof Error) return ctx.fail(e.message); | ||
return ctx.fail(`unknown error: ${e}`); | ||
} | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const executeReadFileContent = async (ctx: PluginContext): Promise<PluginResult> => { | ||
const unsafe = ctx.fetch('path'); | ||
if (typeof unsafe !== 'string') return ctx.fail('path must be string'); | ||
|
||
const { filepath, error } = resolveFilepath(unsafe); | ||
if (!filepath) return ctx.fail(error); | ||
|
||
return ctx.pass(await readFile(unsafe)); | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const executeStoreInFile = async (ctx: PluginContext): Promise<PluginResult> => { | ||
// TODO: should `ctx.fetch('content')` return a JsonableObject? | ||
const content = JSON.stringify(ctx.fetch('content')); | ||
const unsafe = ctx.fetch('path'); | ||
if (typeof unsafe !== 'string') return ctx.fail('path must be string'); | ||
|
||
const { filepath, error } = resolveFilepath(unsafe); | ||
if (!filepath) return ctx.fail(error); | ||
|
||
await fs.mkdir(path.dirname(filepath), { recursive: true }); | ||
await fs.writeFile(filepath, content); | ||
|
||
return ctx.fail('TODO'); | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const executeListDirectoryContent = async (ctx: PluginContext): Promise<PluginResult> => { | ||
const unsafe = ctx.fetch('path'); | ||
if (typeof unsafe !== 'string') return ctx.fail('path must be string'); | ||
|
||
const { dirpath, error } = resolveDirPath(unsafe); | ||
if (!dirpath) return ctx.fail(error); | ||
|
||
const filepaths = (await fs.readdir(dirpath)).map((f) => path.join(dirpath, f)); | ||
const stats = await Promise.all(filepaths.map((f) => fs.stat(f))); | ||
const result = stats.map((stat, i) => { | ||
const filepath = filepaths[i] as string; | ||
return { | ||
name: filepath, | ||
mode: stat.mode.toString(8), | ||
dev: stat.dev, | ||
nlink: stat.nlink, | ||
uid: stat.uid, | ||
gid: stat.gid, | ||
size: stat.size, | ||
blksize: stat.blksize, | ||
blocks: stat.blocks, | ||
atime: stat.atime.toISOString(), | ||
mtime: stat.mtime.toISOString(), | ||
ctime: stat.ctime.toISOString(), | ||
birthtime: stat.birthtime.toISOString(), | ||
}; | ||
}); | ||
return ctx.pass(result); | ||
}; | ||
|
||
const fsPlugin = async (ctx: PluginContext): Promise<PluginResult> => { | ||
switch (ctx.phrase) { | ||
case 'download extract': | ||
return await executeDownloadExtract(ctx); | ||
case 'read file content': | ||
return await executeReadFileContent(ctx); | ||
case 'store in file': | ||
return await executeStoreInFile(ctx); | ||
case 'list directory content': | ||
return await executeListDirectoryContent(ctx); | ||
default: | ||
return ctx.fail('no math'); | ||
} | ||
}; | ||
|
||
export const fsPlugins = new Set([fsPlugin]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import test from 'ava'; | ||
|
||
test('ava is happy', (t) => t.true(true)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../tsconfig.json |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.