commit 5bb80dbc9003e8c1bc9e9bfb25c49443650b83c7 Author: ION606 Date: Wed May 22 20:29:46 2024 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..944cd86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +config.json +tests/config.json \ No newline at end of file diff --git a/customFileSystem.js b/customFileSystem.js new file mode 100644 index 0000000..7ebaaf8 --- /dev/null +++ b/customFileSystem.js @@ -0,0 +1,283 @@ +import axios from "axios"; +import base64 from 'base-64' +// import stream from 'stream'; + +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +class ghelper { + #errhelper(err) { + if (err.response) { + console.error(err.response); + + // switch(err.response.code) { + + // } + // console.log(err.response.status); + // console.log(err.response.headers); + } + else console.error(err); + + return null + } + + async appendToFile(fpath, toAdd) { + try { + const url = `${this.ghurl}/contents/${fpath}`; + + const response = await axios.get(url, { + headers: this.authHeaders, + }).catch((err) => { + if (err.res.statusCode != 404) console.error(err); + return null; + }); + + let content = (response?.data) ? base64.decode(response.data.content) : ""; + content += toAdd; + + return this.addToRepo(fpath, content, response?.data?.sha); + } + catch (err) { + console.error(err); + return null; + } + } + + async addToRepo(fpath, contentRaw, sha = undefined) { + try { + const url = `${this.ghurl}/contents/${fpath}`; + + const content = (!(contentRaw instanceof Buffer)) ? Buffer.from(contentRaw) : contentRaw; + const toAdd = content.toString('base64'); + // Prepare the commit + const updateData = { + message: `added content for ${fpath}`, + content: toAdd, + }; + + if (sha) updateData['sha'] = sha; + + // Commit the update + const updateResponse = await axios.put(url, updateData, { + headers: this.authHeaders + }).catch((err) => this.#errhelper(err)); + + return updateResponse?.data; + } + catch (err) { + console.error(err); + return null; + } + } + + + async remFromRepo(fpath) { + const url = `${this.ghurl}/contents/${fpath}`; + + const resget = await axios.get(url, { + headers: this.authHeaders + }) + .catch(this.#errhelper); + + const sha = resget?.data?.sha; + if (!sha) return; + + const resdel = await axios.delete(url, { + headers: this.authHeaders, + data: { + message: `added content for ${fpath}`, + sha + } + }).catch(this.#errhelper); + + return resdel; + } + + + async readTree(branchName) { + try { + let url = `${this.ghurl}/branches/${branchName}`; + + // Commit the update + let res = await axios.get(url, { + headers: this.authHeaders + }) + .catch(this.#errhelper); + + if (!res) return; + const treesha = res.data.commit.commit.tree.sha; + + url = `${this.ghurl}/git/trees/${treesha}`; + + // Commit the update + res = await axios.get(url, { + headers: this.authHeaders + }) + .catch(this.#errhelper); + + return res?.data; + } + catch (err) { + console.error(err); + return null; + } + } + + + /** + * if dPath is a file, then the contents of the file will be returned, otherwise the directory structure will be returned + * @returns {Promise}} + */ + async getStructure(dPath, getSha = false) { + try { + let url = `${this.ghurl}/contents`; + if (dPath) url += `/${dPath}`; + + // Commit the update + const res = await axios.get(url, { + headers: this.authHeaders + }); + + const { data } = res; + if (getSha) return res?.data?.sha; + + if (Array.isArray(data)) return data.map(o => ({ type: o.type, name: o.name })); + return Buffer.from(data.content, data.encoding); + } + catch (err) { + if (err.response.status != 404) console.error(err); + return false; + } + } + + constructor(repoURL, token) { + this.ghurl = repoURL; + this.authHeaders = { + 'Authorization': `token ${token}` + }; + } +} + + +export class mockFileObj { + /** @type {String} */ + #fpath; + + /** @type {String} */ + #encoding; + + /** @type {Promise[]} */ + #fptrs + + /** @type {customFs} */ + #cfs; + + write(toAdd) { + const r = this.#cfs.appendToFile(this.#fpath, toAdd); + this.#fptrs.push(r); + } + + async close() { + try { + await Promise.all(this.#fptrs); + return true; + } + catch (err) { + console.error(err); + return null; + } + } + + constructor(cfs, fpath, encoding = undefined) { + this.#fpath = fpath; + this.#encoding = encoding; + this.#fptrs = []; + } +} + + +/** + * To replace `fs` by writing to a remote source (github) + */ +export default class customFs { + /** @type {ghelper} */ + ghs; + + async #getFilesInDir(dirPath) { + const all = await this.readdirSync(dirPath); + const files = all.filter(o => o.type === 'file').map(f => `${dirPath}/${f.name}`); + const folders = all.filter(o => o.type === 'dir'); + const subFiles = await Promise.all(folders.map(f => this.#getFilesInDir(`${dirPath}/${f.name}`))); + return files.concat(...subFiles); + } + + async rmSync(dirPath, opts) { + const files = await this.readdirSync(dirPath); + + if (Array.isArray(files) && opts?.recursive) { + // get tree + const subFiles = await this.#getFilesInDir(dirPath); + for (const fname of subFiles) { + await this.ghs.remFromRepo(fname); + } + return subFiles; + } + else return await this.ghs.remFromRepo(dirPath); + } + + appendToFile = (fPath, toWrite) => this.ghs.appendToFile(fPath, toWrite); + + /** + * + * @param {string} fPath + * @param {any} toWriteRaw + * @param {{encoding:string}} opts? + * @returns + */ + async writeFileSync(fPath, toWriteRaw, opts = undefined) { + // { encoding: 'base64' } + const encoding = opts?.encoding; + const toWrite = (encoding) ? Buffer.from(toWriteRaw).toString(encoding) : toWriteRaw; + + const r = await this.existsSync(fPath); + await wait(1000); + return this.ghs.addToRepo(fPath, toWrite, r); + } + + writeFile = (fName, toWrite, cb) => this.writeFileSync(fName, toWrite).then(() => cb()).catch(cb); + + + readFileSync = async (fName) => this.ghs.getStructure(fName); + + /** + * @returns {Promise<[{type: "dir" | "file", name: String}]>} + */ + async readdirSync(dirName) { + const r = await this.ghs.getStructure(dirName); + return (r) ? r : []; + } + + /** + * returns the file sha if found + * @returns {Promise} + */ + async existsSync(p) { + const res = await this.ghs.getStructure(p, true); + return (res) ? res : undefined; + } + + /** + * reads into buffer, then when it end writes it to dest + */ + createWriteStream = (fpath) => new mockFileObj(this, fpath); + + /** + * No need, as github does this for you + */ + async mkdirSync(dirName) { + + } + + constructor(repoUrl, token) { + this.ghs = new ghelper(repoUrl, token); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e27ddd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,112 @@ +{ + "name": "github-to-fs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-to-fs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "base-64": "^1.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7d5c23 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "github-to-fs", + "version": "1.0.0", + "description": "a package that allows you to use github like file storage", + "main": "customFileSystem.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "ION606", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "base-64": "^1.0.0" + }, + "type": "module" +} diff --git a/tests/testmain.js b/tests/testmain.js new file mode 100644 index 0000000..8aa7c4f --- /dev/null +++ b/tests/testmain.js @@ -0,0 +1,20 @@ +import customFs from "../customFileSystem"; +import fs from 'fs'; +const { token } = JSON.parse(fs.readFileSync('config.json')); + +(async () => { + // create the custom FS object + const cfs = new customFs('https://api.github.com/repos/ION606/github-to-fs', token); + + // write the file to the repo + await cfs.writeFileSync('example.txt', 'hello world!'); + + // read the contents of the file + console.log((await cfs.readFileSync('example.txt')).toString()); + + // remove the file + await cfs.rmSync('example.txt'); + + // make sure the file is gone + console.log((await cfs.readFileSync('example.txt')).toString()); +}); \ No newline at end of file