Skip to content

Commit 7dd573e

Browse files
authored
Merge pull request #4384 from FlowFuse/4357-file-api
Implement files api
2 parents cb2ec53 + 26ddd02 commit 7dd573e

File tree

31 files changed

+2221
-45
lines changed

31 files changed

+2221
-45
lines changed

forge/containers/stub/index.js

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99
* @memberof forge.containers.drivers
1010
*
1111
*/
12-
const list = {}
12+
const { normalize } = require('path')
13+
14+
const nrUtil = require('@node-red/util') // eslint-disable-line
15+
1316
const forgeUtils = require('../../db/utils')
1417

18+
const list = {}
19+
const files = {}
20+
1521
module.exports = {
1622
START_DELAY: 500,
1723
STOP_DELAY: 250,
@@ -267,5 +273,153 @@ module.exports = {
267273
...this._app.config.driver.options?.default_stack
268274
}
269275
},
270-
revokeUserToken: async (project, token) => { }
276+
revokeUserToken: async (project, token) => { },
277+
278+
// File API
279+
// Static Assets API
280+
listFiles: async (instance, filePath) => {
281+
if (!list[instance.id] || list[instance.id].state === 'suspended') {
282+
throw new Error('Cannot access instance files')
283+
}
284+
if (!files[instance.id]) {
285+
files[instance.id] = {}
286+
}
287+
const pathDots = filePath.replace('/', '.')
288+
const response = {
289+
meta: {},
290+
files: [],
291+
count: 0
292+
}
293+
try {
294+
const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id]
295+
Object.keys(dir).forEach(entry => {
296+
if (typeof dir[entry] === 'object') {
297+
response.files.push({
298+
name: entry,
299+
type: 'directory',
300+
lastModified: new Date().toISOString()
301+
})
302+
} else {
303+
response.files.push({
304+
name: entry,
305+
type: 'file',
306+
size: dir[entry].length,
307+
lastModified: new Date().toISOString()
308+
})
309+
}
310+
response.count++
311+
})
312+
return response
313+
} catch (err) {
314+
if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) {
315+
const newErr = new Error('not found')
316+
newErr.statusCode = 404
317+
throw newErr
318+
} else {
319+
throw err
320+
}
321+
}
322+
},
323+
324+
updateFile: async (instance, filePath, update) => {
325+
if (!list[instance.id] || list[instance.id].state === 'suspended') {
326+
throw new Error('Cannot access instance files')
327+
}
328+
if (!files[instance.id]) {
329+
files[instance.id] = {}
330+
}
331+
// const pathDots = filePath.replace('/','.')
332+
// const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id]
333+
},
334+
335+
deleteFile: async (instance, filePath) => {
336+
if (!list[instance.id] || list[instance.id].state === 'suspended') {
337+
throw new Error('Cannot access instance files')
338+
}
339+
if (!files[instance.id]) {
340+
files[instance.id] = {}
341+
}
342+
const parts = normalize(filePath).split('/')
343+
const filename = parts.pop()
344+
if (parts.indexOf('..') !== -1) {
345+
if (parts.indexOf('..') === 0) {
346+
const newErr = new Error('not found')
347+
newErr.statusCode = 404
348+
throw newErr
349+
} else {
350+
while (parts.indexOf('..') !== -1) {
351+
parts.splice(parts.indexOf('..') - 1, 2)
352+
}
353+
}
354+
}
355+
const pathDots = parts.join('.')
356+
try {
357+
const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id]
358+
delete dir[filename]
359+
} catch (err) {
360+
if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) {
361+
const newErr = new Error('not found')
362+
newErr.statusCode = 404
363+
throw newErr
364+
} else {
365+
throw err
366+
}
367+
}
368+
},
369+
createDirectory: async (instance, filePath, directoryName) => {
370+
if (!list[instance.id] || list[instance.id].state === 'suspended') {
371+
throw new Error('Cannot access instance files')
372+
}
373+
if (!files[instance.id]) {
374+
files[instance.id] = {}
375+
}
376+
const pathDots = filePath.replace('/', '.')
377+
const nameDots = directoryName.replace('/', '.')
378+
try {
379+
const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id]
380+
nrUtil.util.setObjectProperty(dir, nameDots, {}, true)
381+
} catch (err) {
382+
if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) {
383+
const newErr = new Error('not found')
384+
newErr.statusCode = 404
385+
throw newErr
386+
} else {
387+
throw err
388+
}
389+
}
390+
},
391+
uploadFile: async (instance, filePath, readableStream) => {
392+
if (!list[instance.id] || list[instance.id].state === 'suspended') {
393+
throw new Error('Cannot access instance files')
394+
}
395+
if (!files[instance.id]) {
396+
files[instance.id] = {}
397+
}
398+
const parts = normalize(filePath).split('/')
399+
const filename = parts.pop()
400+
if (parts.indexOf('..') !== -1) {
401+
if (parts.indexOf('..') === 0) {
402+
const newErr = new Error('not found')
403+
newErr.statusCode = 404
404+
throw newErr
405+
} else {
406+
while (parts.indexOf('..') !== -1) {
407+
parts.splice(parts.indexOf('..') - 1, 2)
408+
}
409+
}
410+
}
411+
const pathDots = parts.join('.')
412+
try {
413+
const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id]
414+
dir[filename] = readableStream.toString('utf-8')
415+
} catch (err) {
416+
if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) {
417+
const newErr = new Error('not found')
418+
newErr.statusCode = 404
419+
throw newErr
420+
} else {
421+
throw err
422+
}
423+
}
424+
}
271425
}

forge/containers/wrapper.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,5 +238,42 @@ module.exports = {
238238
}
239239
return value
240240
},
241-
properties: () => this.properties
241+
properties: () => this.properties,
242+
243+
// Static Files API
244+
listFiles: async (instance, filePath) => {
245+
if (this._driver.listFiles) {
246+
return this._driver.listFiles(instance, filePath)
247+
} else {
248+
throw new Error('Driver does not implement file API ')
249+
}
250+
},
251+
updateFile: async (instance, filePath, update) => {
252+
if (this._driver.updateFile) {
253+
return this._driver.updateFile(instance, filePath, update)
254+
} else {
255+
throw new Error('Driver does not implement file API ')
256+
}
257+
},
258+
deleteFile: async (instance, filePath) => {
259+
if (this._driver.deleteFile) {
260+
return this._driver.deleteFile(instance, filePath)
261+
} else {
262+
throw new Error('Driver does not implement file API ')
263+
}
264+
},
265+
createDirectory: async (instance, filePath, directoryName) => {
266+
if (this._driver.createDirectory) {
267+
return this._driver.createDirectory(instance, filePath, directoryName)
268+
} else {
269+
throw new Error('Driver does not implement file API ')
270+
}
271+
},
272+
uploadFile: async (instance, filePath, fileBuffer) => {
273+
if (this._driver.uploadFile) {
274+
return this._driver.uploadFile(instance, filePath, fileBuffer)
275+
} else {
276+
throw new Error('Driver does not implement file API ')
277+
}
278+
}
242279
}

forge/db/models/ProjectSettings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const KEY_HA = 'ha'
1515
const KEY_PROTECTED = 'protected'
1616
const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval'
1717
const KEY_CUSTOM_HOSTNAME = 'customHostname'
18+
const KEY_SHARED_ASSETS = 'sharedAssets'
1819

1920
module.exports = {
2021
KEY_SETTINGS,
@@ -23,6 +24,7 @@ module.exports = {
2324
KEY_PROTECTED,
2425
KEY_HEALTH_CHECK_INTERVAL,
2526
KEY_CUSTOM_HOSTNAME,
27+
KEY_SHARED_ASSETS,
2628
name: 'ProjectSettings',
2729
schema: {
2830
ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' },

forge/ee/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ module.exports = async function (app) {
2323
await app.register(require('./httpTokens'), { prefix: '/api/v1/projects/:projectId/httpTokens', logLevel: app.config.logging.http })
2424
await app.register(require('./customHostnames'), { prefix: '/api/v1/projects/:projectId/customHostname', logLevel: app.config.logging.http })
2525

26+
await app.register(require('./staticAssets'), { prefix: '/api/v1/projects/:projectId/files', logLevel: app.config.logging.http })
27+
2628
// Important: keep SSO last to avoid its error handling polluting other routes.
2729
await app.register(require('./sso'), { logLevel: app.config.logging.http })
2830
}

0 commit comments

Comments
 (0)