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.

280 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 farmhash = require('farmhash')
  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. return
  91. },
  92. /**
  93. * Gets the uploads files.
  94. *
  95. * @param {String} cat Category type
  96. * @param {String} fld Folder
  97. * @return {Array<Object>} The files matching the query
  98. */
  99. getUploadsFiles (cat, fld) {
  100. return db.UplFile.find({
  101. category: cat,
  102. folder: 'f:' + fld
  103. }).sort('filename').exec()
  104. },
  105. /**
  106. * Deletes an uploads file.
  107. *
  108. * @param {string} uid The file unique ID
  109. * @return {Promise} Promise of the operation
  110. */
  111. deleteUploadsFile (uid) {
  112. let self = this
  113. return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
  114. if (f) {
  115. return self.deleteUploadsFileTry(f, 0)
  116. }
  117. return true
  118. })
  119. },
  120. deleteUploadsFileTry (f, attempt) {
  121. let self = this
  122. let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
  123. return Promise.join(
  124. fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
  125. fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
  126. ).catch((err) => {
  127. if (err.code === 'EBUSY' && attempt < 5) {
  128. return Promise.delay(100).then(() => {
  129. return self.deleteUploadsFileTry(f, attempt + 1)
  130. })
  131. } else {
  132. winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.')
  133. return true
  134. }
  135. })
  136. },
  137. /**
  138. * Downloads a file from url.
  139. *
  140. * @param {String} fFolder The folder
  141. * @param {String} fUrl The full URL
  142. * @return {Promise} Promise of the operation
  143. */
  144. downloadFromUrl (fFolder, fUrl) {
  145. let fUrlObj = url.parse(fUrl)
  146. let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'))
  147. let destFolder = _.chain(fFolder).trim().toLower().value()
  148. return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
  149. if (!destFolderPath) {
  150. return Promise.reject(new Error('Invalid Folder'))
  151. }
  152. return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
  153. let destFilePath = path.resolve(destFolderPath, destFilename)
  154. return new Promise((resolve, reject) => {
  155. let rq = request({
  156. url: fUrl,
  157. method: 'GET',
  158. followRedirect: true,
  159. maxRedirects: 5,
  160. timeout: 10000
  161. })
  162. let destFileStream = fs.createWriteStream(destFilePath)
  163. let curFileSize = 0
  164. rq.on('data', (data) => {
  165. curFileSize += data.length
  166. if (curFileSize > maxDownloadFileSize) {
  167. rq.abort()
  168. destFileStream.destroy()
  169. fs.remove(destFilePath)
  170. reject(new Error('Remote file is too large!'))
  171. }
  172. }).on('error', (err) => {
  173. destFileStream.destroy()
  174. fs.remove(destFilePath)
  175. reject(err)
  176. })
  177. destFileStream.on('finish', () => {
  178. resolve(true)
  179. })
  180. rq.pipe(destFileStream)
  181. })
  182. })
  183. })
  184. },
  185. /**
  186. * Move/Rename a file
  187. *
  188. * @param {String} uid The file ID
  189. * @param {String} fld The destination folder
  190. * @param {String} nFilename The new filename (optional)
  191. * @return {Promise} Promise of the operation
  192. */
  193. moveUploadsFile (uid, fld, nFilename) {
  194. let self = this
  195. return db.UplFolder.findById('f:' + fld).then((folder) => {
  196. if (folder) {
  197. return db.UplFile.findById(uid).then((originFile) => {
  198. // -> Check if rename is valid
  199. let nameCheck = null
  200. if (nFilename) {
  201. let originFileObj = path.parse(originFile.filename)
  202. nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
  203. } else {
  204. nameCheck = Promise.resolve(originFile.filename)
  205. }
  206. return nameCheck.then((destFilename) => {
  207. let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
  208. let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
  209. let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
  210. let preMoveOps = []
  211. // -> Check for invalid operations
  212. if (sourceFilePath === destFilePath) {
  213. return Promise.reject(new Error('Invalid Operation!'))
  214. }
  215. // -> Delete DB entry
  216. preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
  217. // -> Move thumbnail ahead to avoid re-generation
  218. if (originFile.category === 'image') {
  219. let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename)
  220. let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
  221. let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
  222. preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
  223. } else {
  224. preMoveOps.push(Promise.resolve(true))
  225. }
  226. // -> Proceed to move actual file
  227. return Promise.all(preMoveOps).then(() => {
  228. return fs.moveAsync(sourceFilePath, destFilePath, {
  229. clobber: false
  230. })
  231. })
  232. })
  233. })
  234. } else {
  235. return Promise.reject(new Error('Invalid Destination Folder'))
  236. }
  237. })
  238. }
  239. }