You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

279 lines
7.9 KiB

  1. 'use strict'
  2. const path = require('path')
  3. const Promise = require('bluebird')
  4. const fs = Promise.promisifyAll(require('fs-extra'))
  5. const request = require('request')
  6. const url = require('url')
  7. const crypto = require('crypto')
  8. const _ = require('lodash')
  9. var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
  10. const maxDownloadFileSize = 3145728 // 3 MB
  11. /**
  12. * Uploads
  13. */
  14. module.exports = {
  15. _uploadsPath: './repo/uploads',
  16. _uploadsThumbsPath: './data/thumbs',
  17. /**
  18. * Initialize Local Data Storage model
  19. *
  20. * @return {Object} Uploads model instance
  21. */
  22. init () {
  23. this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
  24. this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
  25. return this
  26. },
  27. /**
  28. * Gets the thumbnails folder path.
  29. *
  30. * @return {String} The thumbs path.
  31. */
  32. getThumbsPath () {
  33. return this._uploadsThumbsPath
  34. },
  35. /**
  36. * Gets the uploads folders.
  37. *
  38. * @return {Array<String>} The uploads folders.
  39. */
  40. getUploadsFolders () {
  41. return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
  42. return (results) ? _.map(results, 'name') : [{ name: '' }]
  43. })
  44. },
  45. /**
  46. * Creates an uploads folder.
  47. *
  48. * @param {String} folderName The folder name
  49. * @return {Promise} Promise of the operation
  50. */
  51. createUploadsFolder (folderName) {
  52. let self = this
  53. folderName = _.kebabCase(_.trim(folderName))
  54. if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
  55. return Promise.resolve(self.getUploadsFolders())
  56. }
  57. return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
  58. return db.UplFolder.findOneAndUpdate({
  59. _id: 'f:' + folderName
  60. }, {
  61. name: folderName
  62. }, {
  63. upsert: true
  64. })
  65. }).then(() => {
  66. return self.getUploadsFolders()
  67. })
  68. },
  69. /**
  70. * Check if folder is valid and exists
  71. *
  72. * @param {String} folderName The folder name
  73. * @return {Boolean} True if valid
  74. */
  75. validateUploadsFolder (folderName) {
  76. return db.UplFolder.findOne({ name: folderName }).then((f) => {
  77. return (f) ? path.resolve(this._uploadsPath, folderName) : false
  78. })
  79. },
  80. /**
  81. * Adds one or more uploads files.
  82. *
  83. * @param {Array<Object>} arrFiles The uploads files
  84. * @return {Void} Void
  85. */
  86. addUploadsFiles (arrFiles) {
  87. if (_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
  88. // this._uploadsDb.Files.insert(arrFiles);
  89. }
  90. },
  91. /**
  92. * Gets the uploads files.
  93. *
  94. * @param {String} cat Category type
  95. * @param {String} fld Folder
  96. * @return {Array<Object>} The files matching the query
  97. */
  98. getUploadsFiles (cat, fld) {
  99. return db.UplFile.find({
  100. category: cat,
  101. folder: 'f:' + fld
  102. }).sort('filename').exec()
  103. },
  104. /**
  105. * Deletes an uploads file.
  106. *
  107. * @param {string} uid The file unique ID
  108. * @return {Promise} Promise of the operation
  109. */
  110. deleteUploadsFile (uid) {
  111. let self = this
  112. return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
  113. if (f) {
  114. return self.deleteUploadsFileTry(f, 0)
  115. }
  116. return true
  117. })
  118. },
  119. deleteUploadsFileTry (f, attempt) {
  120. let self = this
  121. let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
  122. return Promise.join(
  123. fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
  124. fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
  125. ).catch((err) => {
  126. if (err.code === 'EBUSY' && attempt < 5) {
  127. return Promise.delay(100).then(() => {
  128. return self.deleteUploadsFileTry(f, attempt + 1)
  129. })
  130. } else {
  131. winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.')
  132. return true
  133. }
  134. })
  135. },
  136. /**
  137. * Downloads a file from url.
  138. *
  139. * @param {String} fFolder The folder
  140. * @param {String} fUrl The full URL
  141. * @return {Promise} Promise of the operation
  142. */
  143. downloadFromUrl (fFolder, fUrl) {
  144. let fUrlObj = url.parse(fUrl)
  145. let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'))
  146. let destFolder = _.chain(fFolder).trim().toLower().value()
  147. return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
  148. if (!destFolderPath) {
  149. return Promise.reject(new Error('Invalid Folder'))
  150. }
  151. return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
  152. let destFilePath = path.resolve(destFolderPath, destFilename)
  153. return new Promise((resolve, reject) => {
  154. let rq = request({
  155. url: fUrl,
  156. method: 'GET',
  157. followRedirect: true,
  158. maxRedirects: 5,
  159. timeout: 10000
  160. })
  161. let destFileStream = fs.createWriteStream(destFilePath)
  162. let curFileSize = 0
  163. rq.on('data', (data) => {
  164. curFileSize += data.length
  165. if (curFileSize > maxDownloadFileSize) {
  166. rq.abort()
  167. destFileStream.destroy()
  168. fs.remove(destFilePath)
  169. reject(new Error('Remote file is too large!'))
  170. }
  171. }).on('error', (err) => {
  172. destFileStream.destroy()
  173. fs.remove(destFilePath)
  174. reject(err)
  175. })
  176. destFileStream.on('finish', () => {
  177. resolve(true)
  178. })
  179. rq.pipe(destFileStream)
  180. })
  181. })
  182. })
  183. },
  184. /**
  185. * Move/Rename a file
  186. *
  187. * @param {String} uid The file ID
  188. * @param {String} fld The destination folder
  189. * @param {String} nFilename The new filename (optional)
  190. * @return {Promise} Promise of the operation
  191. */
  192. moveUploadsFile (uid, fld, nFilename) {
  193. let self = this
  194. return db.UplFolder.findById('f:' + fld).then((folder) => {
  195. if (folder) {
  196. return db.UplFile.findById(uid).then((originFile) => {
  197. // -> Check if rename is valid
  198. let nameCheck = null
  199. if (nFilename) {
  200. let originFileObj = path.parse(originFile.filename)
  201. nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
  202. } else {
  203. nameCheck = Promise.resolve(originFile.filename)
  204. }
  205. return nameCheck.then((destFilename) => {
  206. let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
  207. let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
  208. let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
  209. let preMoveOps = []
  210. // -> Check for invalid operations
  211. if (sourceFilePath === destFilePath) {
  212. return Promise.reject(new Error('Invalid Operation!'))
  213. }
  214. // -> Delete DB entry
  215. preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
  216. // -> Move thumbnail ahead to avoid re-generation
  217. if (originFile.category === 'image') {
  218. let fUid = crypto.createHash('md5').update(folder.name + '/' + destFilename).digest('hex')
  219. let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
  220. let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
  221. preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
  222. } else {
  223. preMoveOps.push(Promise.resolve(true))
  224. }
  225. // -> Proceed to move actual file
  226. return Promise.all(preMoveOps).then(() => {
  227. return fs.moveAsync(sourceFilePath, destFilePath, {
  228. clobber: false
  229. })
  230. })
  231. })
  232. })
  233. } else {
  234. return Promise.reject(new Error('Invalid Destination Folder'))
  235. }
  236. })
  237. }
  238. }