video-path-manager.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { Mutex } from 'async-mutex'
  2. import { remove } from 'fs-extra/esm'
  3. import { extname, join } from 'path'
  4. import { FileStorage } from '@peertube/peertube-models'
  5. import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
  6. import { extractVideo } from '@server/helpers/video.js'
  7. import { CONFIG } from '@server/initializers/config.js'
  8. import { DIRECTORIES } from '@server/initializers/constants.js'
  9. import {
  10. MStreamingPlaylistVideo,
  11. MVideo,
  12. MVideoFile,
  13. MVideoFileStreamingPlaylistVideo,
  14. MVideoFileVideo
  15. } from '@server/types/models/index.js'
  16. import { buildUUID } from '@peertube/peertube-node-utils'
  17. import { makeHLSFileAvailable, makeWebVideoFileAvailable } from './object-storage/index.js'
  18. import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths.js'
  19. import { isVideoInPrivateDirectory } from './video-privacy.js'
  20. type MakeAvailableCB <T> = (path: string) => Promise<T> | T
  21. const lTags = loggerTagsFactory('video-path-manager')
  22. class VideoPathManager {
  23. private static instance: VideoPathManager
  24. // Key is a video UUID
  25. private readonly videoFileMutexStore = new Map<string, Mutex>()
  26. private constructor () {}
  27. getFSHLSOutputPath (video: MVideo, filename?: string) {
  28. const base = getHLSDirectory(video)
  29. if (!filename) return base
  30. return join(base, filename)
  31. }
  32. getFSRedundancyVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
  33. if (videoFile.isHLS()) {
  34. const video = extractVideo(videoOrPlaylist)
  35. return join(getHLSRedundancyDirectory(video), videoFile.filename)
  36. }
  37. return join(CONFIG.STORAGE.REDUNDANCY_DIR, videoFile.filename)
  38. }
  39. getFSVideoFileOutputPath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
  40. const video = extractVideo(videoOrPlaylist)
  41. if (videoFile.isHLS()) {
  42. return join(getHLSDirectory(video), videoFile.filename)
  43. }
  44. if (isVideoInPrivateDirectory(video.privacy)) {
  45. return join(DIRECTORIES.VIDEOS.PRIVATE, videoFile.filename)
  46. }
  47. return join(DIRECTORIES.VIDEOS.PUBLIC, videoFile.filename)
  48. }
  49. async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) {
  50. if (videoFile.storage === FileStorage.FILE_SYSTEM) {
  51. return this.makeAvailableFactory(
  52. () => this.getFSVideoFileOutputPath(videoFile.getVideoOrStreamingPlaylist(), videoFile),
  53. false,
  54. cb
  55. )
  56. }
  57. const destination = this.buildTMPDestination(videoFile.filename)
  58. if (videoFile.isHLS()) {
  59. const playlist = (videoFile as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
  60. return this.makeAvailableFactory(
  61. () => makeHLSFileAvailable(playlist, videoFile.filename, destination),
  62. true,
  63. cb
  64. )
  65. }
  66. return this.makeAvailableFactory(
  67. () => makeWebVideoFileAvailable(videoFile.filename, destination),
  68. true,
  69. cb
  70. )
  71. }
  72. async makeAvailableResolutionPlaylistFile <T> (videoFile: MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) {
  73. const filename = getHlsResolutionPlaylistFilename(videoFile.filename)
  74. if (videoFile.storage === FileStorage.FILE_SYSTEM) {
  75. return this.makeAvailableFactory(
  76. () => join(getHLSDirectory(videoFile.getVideo()), filename),
  77. false,
  78. cb
  79. )
  80. }
  81. const playlist = videoFile.VideoStreamingPlaylist
  82. return this.makeAvailableFactory(
  83. () => makeHLSFileAvailable(playlist, filename, this.buildTMPDestination(filename)),
  84. true,
  85. cb
  86. )
  87. }
  88. async makeAvailablePlaylistFile <T> (playlist: MStreamingPlaylistVideo, filename: string, cb: MakeAvailableCB<T>) {
  89. if (playlist.storage === FileStorage.FILE_SYSTEM) {
  90. return this.makeAvailableFactory(
  91. () => join(getHLSDirectory(playlist.Video), filename),
  92. false,
  93. cb
  94. )
  95. }
  96. return this.makeAvailableFactory(
  97. () => makeHLSFileAvailable(playlist, filename, this.buildTMPDestination(filename)),
  98. true,
  99. cb
  100. )
  101. }
  102. async lockFiles (videoUUID: string) {
  103. if (!this.videoFileMutexStore.has(videoUUID)) {
  104. this.videoFileMutexStore.set(videoUUID, new Mutex())
  105. }
  106. const mutex = this.videoFileMutexStore.get(videoUUID)
  107. const releaser = await mutex.acquire()
  108. logger.debug('Locked files of %s.', videoUUID, lTags(videoUUID))
  109. return releaser
  110. }
  111. unlockFiles (videoUUID: string) {
  112. const mutex = this.videoFileMutexStore.get(videoUUID)
  113. mutex.release()
  114. logger.debug('Released lockfiles of %s.', videoUUID, lTags(videoUUID))
  115. }
  116. private async makeAvailableFactory <T> (method: () => Promise<string> | string, clean: boolean, cb: MakeAvailableCB<T>) {
  117. let result: T
  118. const destination = await method()
  119. try {
  120. result = await cb(destination)
  121. } catch (err) {
  122. if (destination && clean) await remove(destination)
  123. throw err
  124. }
  125. if (clean) await remove(destination)
  126. return result
  127. }
  128. private buildTMPDestination (filename: string) {
  129. return join(CONFIG.STORAGE.TMP_DIR, buildUUID() + extname(filename))
  130. }
  131. static get Instance () {
  132. return this.instance || (this.instance = new this())
  133. }
  134. }
  135. // ---------------------------------------------------------------------------
  136. export {
  137. VideoPathManager
  138. }