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.

281 lines
8.1 KiB

  1. 'use strict'
  2. /* global wiki */
  3. const path = require('path')
  4. const Promise = require('bluebird')
  5. const fs = Promise.promisifyAll(require('fs-extra'))
  6. const request = require('request')
  7. const url = require('url')
  8. const crypto = require('crypto')
  9. const _ = require('lodash')
  10. var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
  11. const maxDownloadFileSize = 3145728 // 3 MB
  12. /**
  13. * Uploads
  14. */
  15. module.exports = {
  16. _uploadsPath: './repo/uploads',
  17. _uploadsThumbsPath: './data/thumbs',
  18. /**
  19. * Initialize Local Data Storage model
  20. *
  21. * @return {Object} Uploads model instance
  22. */
  23. init () {
  24. this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
  25. this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
  26. return this
  27. },
  28. /**
  29. * Gets the thumbnails folder path.
  30. *
  31. * @return {String} The thumbs path.
  32. */
  33. getThumbsPath () {
  34. return this._uploadsThumbsPath
  35. },
  36. /**
  37. * Gets the uploads folders.
  38. *
  39. * @return {Array<String>} The uploads folders.
  40. */
  41. getUploadsFolders () {
  42. return wiki.db.Folder.find({}, 'name').sort('name').exec().then((results) => {
  43. return (results) ? _.map(results, 'name') : [{ name: '' }]
  44. })
  45. },
  46. /**
  47. * Creates an uploads folder.
  48. *
  49. * @param {String} folderName The folder name
  50. * @return {Promise} Promise of the operation
  51. */
  52. createUploadsFolder (folderName) {
  53. let self = this
  54. folderName = _.kebabCase(_.trim(folderName))
  55. if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
  56. return Promise.resolve(self.getUploadsFolders())
  57. }
  58. return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
  59. return wiki.db.UplFolder.findOneAndUpdate({
  60. _id: 'f:' + folderName
  61. }, {
  62. name: folderName
  63. }, {
  64. upsert: true
  65. })
  66. }).then(() => {
  67. return self.getUploadsFolders()
  68. })
  69. },
  70. /**
  71. * Check if folder is valid and exists
  72. *
  73. * @param {String} folderName The folder name
  74. * @return {Boolean} True if valid
  75. */
  76. validateUploadsFolder (folderName) {
  77. return wiki.db.UplFolder.findOne({ name: folderName }).then((f) => {
  78. return (f) ? path.resolve(this._uploadsPath, folderName) : false
  79. })
  80. },
  81. /**
  82. * Adds one or more uploads files.
  83. *
  84. * @param {Array<Object>} arrFiles The uploads files
  85. * @return {Void} Void
  86. */
  87. addUploadsFiles (arrFiles) {
  88. if (_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
  89. // this._uploadswiki.Db.Files.insert(arrFiles);
  90. }
  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 wiki.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 wiki.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. wiki.logger.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 wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
  149. if (!destFolderPath) {
  150. return Promise.reject(new Error(wiki.lang.t('errors:invalidfolder')))
  151. }
  152. return wiki.disk.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(wiki.lang.t('errors:remotetoolarge')))
  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 wiki.db.UplFolder.finwiki.dById('f:' + fld).then((folder) => {
  196. if (folder) {
  197. return wiki.db.UplFile.finwiki.dById(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 = wiki.disk.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(wiki.lang.t('errors:invalidoperation')))
  214. }
  215. // -> Delete wiki.DB entry
  216. preMoveOps.push(wiki.db.UplFile.finwiki.dByIdAndRemove(uid))
  217. // -> Move thumbnail ahead to avoid re-generation
  218. if (originFile.category === 'image') {
  219. let fUid = crypto.createHash('md5').update(folder.name + '/' + destFilename).digest('hex')
  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(wiki.lang.t('errors:invaliddestfolder')))
  236. }
  237. })
  238. }
  239. }