-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathrestapi.plugin.coffee
More file actions
534 lines (435 loc) · 14.4 KB
/
restapi.plugin.coffee
File metadata and controls
534 lines (435 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# Prepare
pathUtil = require('path')
# Export Plugin
module.exports = (BasePlugin) ->
# Define Plugin
class RestAPI extends BasePlugin
# Plugin name
name: 'restapi'
config:
channel: '/restapi'
maxFilenameLen: 40
injectHelper: null
collectionPathMap: null
getCollectionPath: (collectionName) ->
config = @getConfig()
docpadConfig = @docpad.getConfig()
collectionPath = docpadConfig[collectionName+'Paths']?[0] or config.collectionPathMap?[collectionName]
defaultCollectionPath = docpadConfig['documentsPaths']?[0]
if collectionPath is null
docpad.log('warn', "The file path for the collection #{collectionName} could not be determined, defaulting to #{defaultCollectionPath}")
return collectionPath or defaultCollectionPath
# Server Extend Event
# Add all of our REST Routes
serverExtend: (opts) ->
plugin = @
docpad = @docpad
{server} = opts
{channel} = @getConfig()
# Send Reslut
sendSuccessData = (res,data,message) ->
res.send(
success: true
message: message or "Action completed successfully"
data: data
)
# Send Success Message
sendSuccessMessage = (res,message) ->
res.send(
success: true
message: message
)
# Send Error
sendError = (res,err) ->
res.send(
success: false
message: err.message+': \n'+err.stack.toString()
)
# Send Unauthorized
sendUnauthorized = (res) ->
res.status(403).send(
success: false
message: "Unauthorized"
)
# Prepare file data for sending
prepareFile = (file, additionalFields) ->
# Prepare
result = {}
fields = ['id', 'filename', 'relativePath', 'url', 'urls', 'contentType', 'encoding', 'source', 'content', 'contentRendered', 'date']
additionalFields ?= []
additionalFields = String(additionalFields).split(/[,\s]+/) unless Array.isArray(additionalFields)
# Give the user all fields if they want that
if additionalFields.length is 1 and additionalFields[0] is 'all'
result = file.toJSON()
# Otherwise give the user specific fields
else
result.meta = file.getMeta().toJSON()
for field in fields.concat(additionalFields)
result[field] = file.get(field)
# Get it working
if result.meta.layout
result.meta.layout = file.get('layoutRelativePath')
# return the result
return result
# Prepare collection data for sending
prepareCollection = (collection, additionalFields) ->
result = []
collection.each (file) ->
result.push prepareFile(file, additionalFields)
return result
# Prepare collections
prepareCollections = ->
result = []
addCollection = (collection) ->
relativePaths = []
#ids = []
collection.each (model) ->
relativePath = model.get('relativePath')
relativePaths.push(relativePath) if relativePath
#ids.push(model.id) if model.id
result.push
name: collection.options.name
length: collection.length
relativePaths: relativePaths
#ids: ids
docpad.eachCollection(addCollection)
return result
# Get Unique Filename
# TODO: How should this work with files name jquery.min.js ?
getUniqueRelativePath = (relativePath) ->
# Prepare
result = relativePath
extensions = relativePath.replace(/^.+?\./, '')
# Iterate while found
while (file = docpad.getDatabase().where({relativeBase: result.replace(/\..*$/, '')})[0])
# test.txt
# > test-2.html
# > test-3.html.md
basename = file.get('basename')
relativeDirPath = file.get('relativeDirPath')
parts = /^(.+?)-([0-9]+)$/.exec(basename)
if parts
basename = parts[1]+'-'+(parseInt(parts[2], 10)+1)
else
basename += '-2'
result = relativeDirPath+'/'+basename
result += '.'+extensions if extensions
# Return
return result
# Get files from request
# next(err, files, file)
# return files/files
getFilesFromRequest = (req,next) ->
# Prepare
files = null
queryOpts = null
sortOpts = null
pageOpts = null
# Extract
relativePath = req.params[0] or null
collectionName = req.params.collectionName
mime = req.query.mime or null
extension = req.query.extension or null
page = req.query.page or null
limit = req.query.limit ? null
offset = req.query.offset ? null
filter = req.query.filter
# Check
collection = docpad.getCollection(collectionName)
unless collection
err = new Error("Couldn't find the collection: #{collectionName}")
return next(err); err
# Add paging
if page? or limit? or offset?
pageOpts ?= {}
pageOpts.page = parseInt(page, 10) if page?
pageOpts.limit = parseInt(limit, 10) if limit?
pageOpts.offset = parseInt(offset, 10) if offset?
# Add filter to query
if filter
try
queryOpts = JSON.parse(filter)
catch err
err = new Error("Failed to parse your custom filter: #{JSON.stringify(filter)}")
return next(err); err
# Add the relative path to the query
if relativePath
queryOpts ?= {}
queryOpts.$or =
relativePath: relativePath
relativeDirPath: relativePath.replace(/[\/\\]+$/, '')
# Add extension to query
if extension
queryOpts ?= {}
queryOpts.extensions = $has: extension
# Add mime to query
if mime
queryOpts ?= {}
queryOpts.outContentType = $like: mime
# Perform filters
result =
if queryOpts or sortOpts or pageOpts
collection.findAll(queryOpts, sortOpts, pageOpts)
else
collection
# Return
return next(null, result); result
# Delete files from request
# next(err, files)
# return err/files
deleteFilesFromRequest = (req,next) ->
# Import
{TaskGroup} = require('taskgroup')
# Fetch
return getFilesFromRequest req, (err, files) ->
# Check
return next(err) if err
return next(null, files) if files.length is 0
# Delete and remove files from database in parallel
# Create task group
tasks = new TaskGroup(concurrency:0).done (err) ->
# Check
return next(err) if err
# Generate
docpad.action 'generate', {reset:false}, (err) ->
return next(err, files)
# Add tasks
files.each (file) -> tasks.addTask (complete) ->
file.deleteSource (err) ->
return complete(err) if err
docpad.getDatabase().remove(file)
return complete()
# Run
tasks.run()
# Create a new file from request
# next(err)
# return err/file
createFileFromRequest = (req,next) ->
# Prepare
docpadConfig = docpad.getConfig()
config = plugin.getConfig()
# Extract
collectionName = req.params.collectionName
relativePath = req.params[0]
# Check
collection = docpad.getCollection(collectionName)
unless collection
err = new Error("Couldn't find the collection: #{collectionName}")
return next(err); err
# Check
unless relativePath
err = new Error("No relativePath to place the file specified")
return next(err); err
# Ensure unique filename
relativePath = getUniqueRelativePath(relativePath)
collectionPath = plugin.getCollectionPath(collectionName)
fullPath = pathUtil.resolve(collectionPath, relativePath) if collectionPath
# Set up our meta attributes
fileMetaAttributes = {}
for own key,value of req.body
fileMetaAttributes[key] = value unless key in ['content']
# Set up our attributes
fileAttributes =
data: req.body.content or ''
relativePath: relativePath
fullPath: fullPath
meta: fileMetaAttributes
# Create the file, inject helper and add the file to the database
file = docpad.createModel(fileAttributes)
# Inject helper
config.injectHelper?.call(plugin, file)
# Set up our write source options
writeSourceOptions = {}
writeSourceOptions.content = req.body.content if req.body.content?
# Write source
file.action 'writeSource', writeSourceOptions, (err) ->
# Check
return next(err, file) if err
# Load file
file.action 'load', (err) ->
# Check
return next(err, file) if err
# Add it to the database
docpad.addModel(file)
# Log
docpad.log('info', "Created file #{file.getFilePath()} from request")
# Generate
docpad.action 'generate', (err) ->
return next(err, file)
# Return the created file
return file
# Update file from request
# next(err)
# return err/file
updateFileFromRequest = (req,next) ->
# Extract
collectionName = req.params.collectionName
relativePath = req.params[0]
# Check
collection = docpad.getCollection(collectionName)
unless collection
err = new Error("Couldn't find the collection: #{collectionName}")
return next(err); err
# Check
unless relativePath
err = new Error("No relativePath to find the file specified")
return next(err); err
# Find
file = collection.where({relativePath})[0]
unless file
err = new Error("Couldn't find the file at the relative path: #{relativePath}")
return next(err); err
# Set up our meta attributes
setMeta = false
fileMetaAttributes = {}
for own key,value of req.body
setMeta = true
fileMetaAttributes[key] = value unless key in ['content']
file.setMeta(fileMetaAttributes) if setMeta
# Set up our write source options
writeSourceOptions = {}
writeSourceOptions.content = req.body.content if req.body.content?
# Write source
file.action 'writeSource', writeSourceOptions, (err) ->
# Check
return next(err, file) if err
# Load file
file.action 'load', (err) ->
# Check
return next(err, file) if err
# Log
docpad.log('info', "Updated file #{file.getFilePath()} from request")
# Generate
docpad.action 'generate', (err) ->
return next(err, file)
# Return the created file
return file
###
# Upload a file
server.post "#{channel}/upload", (req, res) ->
# Requires
safefs = require('safefs')
successful = []
failed = []
currentlyUploading = []
count = 0
uploadFile = (file) ->
path = file.path
origName = name = docpadConfig.filesPaths[0] + '/' + file.name
renameCounter = 0
# save an uploaded file
save = -> safefs.rename path, name, (err) ->
unless err
if renameCounter
successful.push
origName: file.name
newName: name.replace(docpadConfig.filesPaths[0] + '/', '')
else
successful.push
name: file.name
else
console.log err
failed.push
file: file.name
error: err
unless --count
if successful.length + failed.length is 1
return res.send(if successful.length then (success: successful) else (success: false, error: failed))
res.send
success: successful
error: failed
# Save an uploaded file with a unique name
saveUnique = (exists) ->
# Name is not unique, try again
if (exists or currentlyUploading.indexOf(name) > -1)
name = origName.replace(/(.*?)(\..*)/, '$1-' + (++renameCounter) + '$2')
return safefs.exists(name, saveUnique)
# Unique name found, let's save it
currentlyUploading.push(name);
save()
# Save each uploaded file
safefs.exists(name, saveUnique)
# Iterate through each uploaded file
for own key of req.files
if req.files[key].name
count++
uploadFile req.files[key]
# If no work to be done, let the user know
unless count
res.send(error: 'No Files specified')
###
# CORS
server.all "#{channel}/*", (req,res,next) ->
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type,X-Requested-With');
return next()
# Fetch the templateData
server.all "#{channel}/template-data/", (req,res) ->
result = docpad.getTemplateData()
return sendSuccessData(res, result, "Listing of template data completed successfully")
# Fetch the files
server.all "#{channel}/files/", (req,res) ->
return res.redirect(301, "#{channel}/collection/database/")
# Fetch the files
server.all "#{channel}/file/*", (req,res) ->
return res.redirect(301, "#{channel}/collection/database/#{req.params[0]}")
# Fetch the collections
server.all "#{channel}/collections/", (req,res) ->
result = prepareCollections()
return sendSuccessData(res, result, "Listing of collections completed successfully")
# CRUD on collections and files
server.all "#{channel}/collection/:collectionName/*", (req,res) ->
# Prepare
method = req.method.toLowerCase()
# Check readonly
if plugin.config.readonly and method isnt 'get'
sendUnauthorized(res)
# GET / READ
else if method is 'get'
# Fetch
collectionName = req.params.collectionName
relativePath = req.params[0]
additionalFields = req.query.additionalFields or req.query.additionalfields
# List
getFilesFromRequest req, (err, files) ->
# Check
return sendError(res, err) if err
# Send
return sendSuccessData(res, prepareCollection(files, additionalFields), "Listing of #{collectionName} at #{relativePath} completed successfully")
# DELETE
else if method is 'delete'
# List
deleteFilesFromRequest req, (err, files) ->
# Check
return sendError(res, err) if err
# Send
return sendSuccessData(res, prepareCollection(files, additionalFields), "Delete completed successfully")
# PUT / CREATE
else if method is 'put'
# Fetch
additionalFields = req.query.additionalFields or req.query.additionalfields
# Create
createFileFromRequest req, (err,file) ->
# Check
return sendError(res, err) if err
# Send
return sendSuccessData(res, prepareFile(file, additionalFields), "Creation completed successfully")
# POST / UPDATE
else if method is 'post'
# Fetch
additionalFields = req.query.additionalFields or req.query.additionalfields
# Update
updateFileFromRequest req, (err,file) ->
# Check
return sendError(res, err) if err
# Send
return sendSuccessData(res, prepareFile(file, additionalFields), "Update completed successfully")
# Unknown
else
err = Error("Unknown method: #{method}")
sendError(res, err)
# Done
return
# Chain
@