@flowfuse/file-server 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/LICENSE +178 -0
- package/README.md +231 -0
- package/etc/flowforge-storage.local.yml +22 -0
- package/etc/flowforge-storage.yml +26 -0
- package/flowforge-file-server-1.14.0.tgz +0 -0
- package/forge/auth.js +106 -0
- package/forge/config.js +93 -0
- package/forge/context-driver/quotaTools.js +53 -0
- package/forge/context-driver/sequelize.js +401 -0
- package/forge/driver.js +19 -0
- package/forge/drivers/localfs.js +142 -0
- package/forge/drivers/memory.js +72 -0
- package/forge/drivers/s3.js +143 -0
- package/forge/drivers/vfs.js +33 -0
- package/forge/fileServer.js +99 -0
- package/forge/routes/context.js +186 -0
- package/forge/routes/files.js +130 -0
- package/forge/routes/index.js +18 -0
- package/forge/routes/quota.js +9 -0
- package/index.js +63 -0
- package/package.json +63 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Storage API
|
|
3
|
+
*
|
|
4
|
+
* @namespace files
|
|
5
|
+
* @memberof forge.fileserver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('fastify')} Fastify */
|
|
9
|
+
/** @typedef {import('fastify').FastifyReply} FastifyReply */
|
|
10
|
+
/** @typedef {import('fastify').FastifyRequest} FastifyRequest */
|
|
11
|
+
|
|
12
|
+
module.exports = async function (app, opts, done) {
|
|
13
|
+
app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, function (request, payload, done) {
|
|
14
|
+
done(null, payload)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create/Update a File
|
|
19
|
+
*
|
|
20
|
+
* @name /v1/files/:teamId/:projectId/*
|
|
21
|
+
* @static
|
|
22
|
+
* @memberof forge.fileserver.files
|
|
23
|
+
*/
|
|
24
|
+
app.post('/:teamId/:projectId/*', {
|
|
25
|
+
|
|
26
|
+
}, async (request, reply) => {
|
|
27
|
+
const path = request.params['*']
|
|
28
|
+
let quota = -1
|
|
29
|
+
if (request.quota?.file) {
|
|
30
|
+
quota = request.quota.file
|
|
31
|
+
} else if (app.config.driver.quota) {
|
|
32
|
+
quota = app.config.driver.quota
|
|
33
|
+
}
|
|
34
|
+
let currentSize = -1
|
|
35
|
+
if (quota !== -1) {
|
|
36
|
+
currentSize = await request.vfs.quota()
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
if (request.headers.ff_mode === 'append') {
|
|
40
|
+
if (currentSize !== -1) {
|
|
41
|
+
const newSize = currentSize + request.body.length
|
|
42
|
+
if (newSize < quota) {
|
|
43
|
+
await request.vfs.append(path, request.body)
|
|
44
|
+
} else {
|
|
45
|
+
reply.code(413).send({ code: 'over_quota', error: 'Over Quota', limit: quota })
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
await request.vfs.append(path, request.body)
|
|
50
|
+
}
|
|
51
|
+
} else if (request.headers.ff_mode === 'ensureDir') {
|
|
52
|
+
await request.vfs.ensureDir(path, request.body)
|
|
53
|
+
} else {
|
|
54
|
+
// should really check if file exists first and compute the delta
|
|
55
|
+
if (currentSize !== -1) {
|
|
56
|
+
const newSize = currentSize + request.body.length
|
|
57
|
+
if (newSize < quota) {
|
|
58
|
+
await request.vfs.save(path, request.body)
|
|
59
|
+
} else {
|
|
60
|
+
reply.code(413).send({ code: 'over_quota', error: 'Over Quota', limit: quota })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
await request.vfs.save(path, request.body)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
reply.code(200).send()
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(err)
|
|
70
|
+
reply.code(400).send(err)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// /**
|
|
75
|
+
// * Stat a file
|
|
76
|
+
// *
|
|
77
|
+
// * @name /v1/files/:teamId/:projectId/*
|
|
78
|
+
// * @static
|
|
79
|
+
// * @memberof forge.fileserver.files
|
|
80
|
+
// */
|
|
81
|
+
// app.head('/:teamId/:projectId/*', async (request, reply) => {
|
|
82
|
+
|
|
83
|
+
// })
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Retrieve a file
|
|
87
|
+
*
|
|
88
|
+
* @name /v1/files/:teamId/:projectId/*
|
|
89
|
+
* @static
|
|
90
|
+
* @memberof forge.fileserver.files
|
|
91
|
+
*/
|
|
92
|
+
app.get('/:teamId/:projectId/*', async (/** @type {FastifyRequest} */ request, /** @type {FastifyReply} */ reply) => {
|
|
93
|
+
const path = request.params['*']
|
|
94
|
+
try {
|
|
95
|
+
const file = await request.vfs.read(path)
|
|
96
|
+
if (file) {
|
|
97
|
+
reply.send(file)
|
|
98
|
+
} else {
|
|
99
|
+
reply.code(404).send()
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err.code === 'ENOENT') {
|
|
103
|
+
reply.code(404).send(err)
|
|
104
|
+
} else if (err.code === 'ENOTDIR') {
|
|
105
|
+
reply.code(400).send(err)
|
|
106
|
+
} else {
|
|
107
|
+
reply.code(500).send(err)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete a file
|
|
114
|
+
*
|
|
115
|
+
* @name /v1/files/:teamId/:projectId/*
|
|
116
|
+
* @static
|
|
117
|
+
* @memberof forge.fileserver.files
|
|
118
|
+
*/
|
|
119
|
+
app.delete('/:teamId/:projectId/*', async (request, reply) => {
|
|
120
|
+
const path = request.params['*']
|
|
121
|
+
try {
|
|
122
|
+
await request.vfs.delete(path)
|
|
123
|
+
reply.code(200).send()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
reply.code(err.statusCode || 400).send(err)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
done()
|
|
130
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const fp = require('fastify-plugin')
|
|
2
|
+
|
|
3
|
+
module.exports = fp(async function (app, opts, done) {
|
|
4
|
+
app.addHook('preHandler', app.checkAuth)
|
|
5
|
+
await app.register(require('./files'), { prefix: '/v1/files', logLevel: app.config.logging.http })
|
|
6
|
+
await app.register(require('./quota'), { prefix: '/v1/quota', logLevel: app.config.logging.http })
|
|
7
|
+
if (app.config.context?.type) {
|
|
8
|
+
await app.register(require('./context'), { prefix: '/v1/context', logLevel: app.config.logging.http })
|
|
9
|
+
}
|
|
10
|
+
app.decorate('getPaginationOptions', (request, defaults) => {
|
|
11
|
+
const result = { ...defaults, ...request.query }
|
|
12
|
+
if (result.query) {
|
|
13
|
+
result.query = result.query.trim()
|
|
14
|
+
}
|
|
15
|
+
return result
|
|
16
|
+
})
|
|
17
|
+
done()
|
|
18
|
+
})
|
package/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const semver = require('semver')
|
|
5
|
+
const flowForgeFileServer = require('./forge/fileServer')
|
|
6
|
+
const YAML = require('yaml')
|
|
7
|
+
|
|
8
|
+
const app = (async function () {
|
|
9
|
+
if (!semver.satisfies(process.version, '>=16.0.0')) {
|
|
10
|
+
console.error(`FlowForge File Server requires at least NodeJS v16, ${process.version} found`)
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const options = {}
|
|
16
|
+
// The tests for `nr-persistent-context` run a local copy of the file-server
|
|
17
|
+
// and pass in the full config via this env var
|
|
18
|
+
if (process.env.FF_FS_TEST_CONFIG) {
|
|
19
|
+
try {
|
|
20
|
+
options.config = YAML.parse(process.env.FF_FS_TEST_CONFIG)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error('Failed to parse FF_FS_TEST_CONFIG:', err)
|
|
23
|
+
process.exitCode = 1
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const server = await flowForgeFileServer(options)
|
|
29
|
+
let stopping = false
|
|
30
|
+
async function exitWhenStopped () {
|
|
31
|
+
if (!stopping) {
|
|
32
|
+
stopping = true
|
|
33
|
+
server.log.info('Stopping FlowForge File Server')
|
|
34
|
+
await server.close()
|
|
35
|
+
server.log.info('FlowForge File Server stopped')
|
|
36
|
+
process.exit(0)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.on('SIGINT', exitWhenStopped)
|
|
41
|
+
process.on('SIGTERM', exitWhenStopped)
|
|
42
|
+
process.on('SIGHUP', exitWhenStopped)
|
|
43
|
+
process.on('SIGUSR2', exitWhenStopped) // for nodemon restart
|
|
44
|
+
process.on('SIGBREAK', exitWhenStopped)
|
|
45
|
+
process.on('message', function (m) { // for PM2 under window with --shutdown-with-message
|
|
46
|
+
if (m === 'shutdown') { exitWhenStopped() }
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Start the server
|
|
50
|
+
server.listen({ port: server.config.port, host: server.config.host }, function (err, address) {
|
|
51
|
+
if (err) {
|
|
52
|
+
console.error(err)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
return server
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(err)
|
|
59
|
+
process.exitCode = 1
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
|
|
63
|
+
module.exports = app
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowfuse/file-server",
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "A basic Object Storage backend",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js",
|
|
8
|
+
"serve": "cross-env NODE_ENV=development nodemon -w forge index.js",
|
|
9
|
+
"lint": "eslint -c .eslintrc \"forge/**/*.js\" \"test/**/*.js\" index.js",
|
|
10
|
+
"lint:fix": "eslint -c .eslintrc \"forge/**/*.js\" \"test/**/*.js\" index.js --fix",
|
|
11
|
+
"test": "mocha test/unit/**/*_spec.js --timeout 5000",
|
|
12
|
+
"test:nopg": "cross-env TEST_POSTGRES=false mocha test/unit/**/*_spec.js --timeout 5000"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"ff-file-storage": "./index.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"flowfuse",
|
|
19
|
+
"object-store"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/FlowFuse/file-server.git"
|
|
24
|
+
},
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "FlowFuse Inc."
|
|
27
|
+
},
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/FlowFuse/file-server/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/FlowFuse/file-server#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@aws-sdk/client-s3": "^3.388.0",
|
|
35
|
+
"@fastify/helmet": "^11.0.0",
|
|
36
|
+
"@node-red/util": "^3.1.0",
|
|
37
|
+
"fastify": "^4.21.0",
|
|
38
|
+
"fastify-metrics": "^10.3.0",
|
|
39
|
+
"fastify-plugin": "^4.5.1",
|
|
40
|
+
"got": "^11.8.6",
|
|
41
|
+
"pg": "^8.11.2",
|
|
42
|
+
"pino": "^8.15.1",
|
|
43
|
+
"pino-pretty": "^10.2.0",
|
|
44
|
+
"semver": "^7.5.4",
|
|
45
|
+
"sequelize": "^6.33.0",
|
|
46
|
+
"sqlite3": "^5.1.6",
|
|
47
|
+
"yaml": "^2.1.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"cross-env": "^7.0.3",
|
|
51
|
+
"eslint": "^8.47.0",
|
|
52
|
+
"eslint-config-standard": "^17.1.0",
|
|
53
|
+
"eslint-plugin-no-only-tests": "^3.1.0",
|
|
54
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
55
|
+
"mocha": "^10.1.0",
|
|
56
|
+
"mocha-cli": "^1.0.1",
|
|
57
|
+
"nodemon": "^3.0.1",
|
|
58
|
+
"should": "^13.2.3"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=16.x"
|
|
62
|
+
}
|
|
63
|
+
}
|