aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/object-storage
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/object-storage')
-rw-r--r--server/lib/object-storage/shared/object-storage-helpers.ts192
-rw-r--r--server/lib/object-storage/urls.ts29
-rw-r--r--server/lib/object-storage/videos.ts80
3 files changed, 244 insertions, 57 deletions
diff --git a/server/lib/object-storage/shared/object-storage-helpers.ts b/server/lib/object-storage/shared/object-storage-helpers.ts
index c131977e8..05b52f412 100644
--- a/server/lib/object-storage/shared/object-storage-helpers.ts
+++ b/server/lib/object-storage/shared/object-storage-helpers.ts
@@ -2,18 +2,21 @@ import { createReadStream, createWriteStream, ensureDir, ReadStream } from 'fs-e
2import { dirname } from 'path' 2import { dirname } from 'path'
3import { Readable } from 'stream' 3import { Readable } from 'stream'
4import { 4import {
5 _Object,
5 CompleteMultipartUploadCommandOutput, 6 CompleteMultipartUploadCommandOutput,
6 DeleteObjectCommand, 7 DeleteObjectCommand,
7 GetObjectCommand, 8 GetObjectCommand,
8 ListObjectsV2Command, 9 ListObjectsV2Command,
9 PutObjectCommandInput 10 PutObjectAclCommand,
11 PutObjectCommandInput,
12 S3Client
10} from '@aws-sdk/client-s3' 13} from '@aws-sdk/client-s3'
11import { Upload } from '@aws-sdk/lib-storage' 14import { Upload } from '@aws-sdk/lib-storage'
12import { pipelinePromise } from '@server/helpers/core-utils' 15import { pipelinePromise } from '@server/helpers/core-utils'
13import { isArray } from '@server/helpers/custom-validators/misc' 16import { isArray } from '@server/helpers/custom-validators/misc'
14import { logger } from '@server/helpers/logger' 17import { logger } from '@server/helpers/logger'
15import { CONFIG } from '@server/initializers/config' 18import { CONFIG } from '@server/initializers/config'
16import { getPrivateUrl } from '../urls' 19import { getInternalUrl } from '../urls'
17import { getClient } from './client' 20import { getClient } from './client'
18import { lTags } from './logger' 21import { lTags } from './logger'
19 22
@@ -44,69 +47,91 @@ async function storeObject (options: {
44 inputPath: string 47 inputPath: string
45 objectStorageKey: string 48 objectStorageKey: string
46 bucketInfo: BucketInfo 49 bucketInfo: BucketInfo
50 isPrivate: boolean
47}): Promise<string> { 51}): Promise<string> {
48 const { inputPath, objectStorageKey, bucketInfo } = options 52 const { inputPath, objectStorageKey, bucketInfo, isPrivate } = options
49 53
50 logger.debug('Uploading file %s to %s%s in bucket %s', inputPath, bucketInfo.PREFIX, objectStorageKey, bucketInfo.BUCKET_NAME, lTags()) 54 logger.debug('Uploading file %s to %s%s in bucket %s', inputPath, bucketInfo.PREFIX, objectStorageKey, bucketInfo.BUCKET_NAME, lTags())
51 55
52 const fileStream = createReadStream(inputPath) 56 const fileStream = createReadStream(inputPath)
53 57
54 return uploadToStorage({ objectStorageKey, content: fileStream, bucketInfo }) 58 return uploadToStorage({ objectStorageKey, content: fileStream, bucketInfo, isPrivate })
55} 59}
56 60
57// --------------------------------------------------------------------------- 61// ---------------------------------------------------------------------------
58 62
59async function removeObject (filename: string, bucketInfo: BucketInfo) { 63function updateObjectACL (options: {
60 const command = new DeleteObjectCommand({ 64 objectStorageKey: string
65 bucketInfo: BucketInfo
66 isPrivate: boolean
67}) {
68 const { objectStorageKey, bucketInfo, isPrivate } = options
69
70 const key = buildKey(objectStorageKey, bucketInfo)
71
72 logger.debug('Updating ACL file %s in bucket %s', key, bucketInfo.BUCKET_NAME, lTags())
73
74 const command = new PutObjectAclCommand({
61 Bucket: bucketInfo.BUCKET_NAME, 75 Bucket: bucketInfo.BUCKET_NAME,
62 Key: buildKey(filename, bucketInfo) 76 Key: key,
77 ACL: getACL(isPrivate)
63 }) 78 })
64 79
65 return getClient().send(command) 80 return getClient().send(command)
66} 81}
67 82
68async function removePrefix (prefix: string, bucketInfo: BucketInfo) { 83function updatePrefixACL (options: {
69 const s3Client = getClient() 84 prefix: string
70 85 bucketInfo: BucketInfo
71 const commandPrefix = bucketInfo.PREFIX + prefix 86 isPrivate: boolean
72 const listCommand = new ListObjectsV2Command({ 87}) {
73 Bucket: bucketInfo.BUCKET_NAME, 88 const { prefix, bucketInfo, isPrivate } = options
74 Prefix: commandPrefix 89
90 logger.debug('Updating ACL of files in prefix %s in bucket %s', prefix, bucketInfo.BUCKET_NAME, lTags())
91
92 return applyOnPrefix({
93 prefix,
94 bucketInfo,
95 commandBuilder: obj => {
96 return new PutObjectAclCommand({
97 Bucket: bucketInfo.BUCKET_NAME,
98 Key: obj.Key,
99 ACL: getACL(isPrivate)
100 })
101 }
75 }) 102 })
103}
76 104
77 const listedObjects = await s3Client.send(listCommand) 105// ---------------------------------------------------------------------------
78 106
79 // FIXME: use bulk delete when s3ninja will support this operation 107function removeObject (objectStorageKey: string, bucketInfo: BucketInfo) {
80 // const deleteParams = { 108 const key = buildKey(objectStorageKey, bucketInfo)
81 // Bucket: bucketInfo.BUCKET_NAME,
82 // Delete: { Objects: [] }
83 // }
84 109
85 if (isArray(listedObjects.Contents) !== true) { 110 logger.debug('Removing file %s in bucket %s', key, bucketInfo.BUCKET_NAME, lTags())
86 const message = `Cannot remove ${commandPrefix} prefix in bucket ${bucketInfo.BUCKET_NAME}: no files listed.`
87 111
88 logger.error(message, { response: listedObjects, ...lTags() }) 112 const command = new DeleteObjectCommand({
89 throw new Error(message) 113 Bucket: bucketInfo.BUCKET_NAME,
90 } 114 Key: key
91 115 })
92 for (const object of listedObjects.Contents) {
93 const command = new DeleteObjectCommand({
94 Bucket: bucketInfo.BUCKET_NAME,
95 Key: object.Key
96 })
97
98 await s3Client.send(command)
99 116
100 // FIXME: use bulk delete when s3ninja will support this operation 117 return getClient().send(command)
101 // deleteParams.Delete.Objects.push({ Key: object.Key }) 118}
102 }
103 119
120function removePrefix (prefix: string, bucketInfo: BucketInfo) {
104 // FIXME: use bulk delete when s3ninja will support this operation 121 // FIXME: use bulk delete when s3ninja will support this operation
105 // const deleteCommand = new DeleteObjectsCommand(deleteParams)
106 // await s3Client.send(deleteCommand)
107 122
108 // Repeat if not all objects could be listed at once (limit of 1000?) 123 logger.debug('Removing prefix %s in bucket %s', prefix, bucketInfo.BUCKET_NAME, lTags())
109 if (listedObjects.IsTruncated) await removePrefix(prefix, bucketInfo) 124
125 return applyOnPrefix({
126 prefix,
127 bucketInfo,
128 commandBuilder: obj => {
129 return new DeleteObjectCommand({
130 Bucket: bucketInfo.BUCKET_NAME,
131 Key: obj.Key
132 })
133 }
134 })
110} 135}
111 136
112// --------------------------------------------------------------------------- 137// ---------------------------------------------------------------------------
@@ -138,14 +163,42 @@ function buildKey (key: string, bucketInfo: BucketInfo) {
138 163
139// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
140 165
166async function createObjectReadStream (options: {
167 key: string
168 bucketInfo: BucketInfo
169 rangeHeader: string
170}) {
171 const { key, bucketInfo, rangeHeader } = options
172
173 const command = new GetObjectCommand({
174 Bucket: bucketInfo.BUCKET_NAME,
175 Key: buildKey(key, bucketInfo),
176 Range: rangeHeader
177 })
178
179 const response = await getClient().send(command)
180
181 return response.Body as Readable
182}
183
184// ---------------------------------------------------------------------------
185
141export { 186export {
142 BucketInfo, 187 BucketInfo,
143 buildKey, 188 buildKey,
189
144 storeObject, 190 storeObject,
191
145 removeObject, 192 removeObject,
146 removePrefix, 193 removePrefix,
194
147 makeAvailable, 195 makeAvailable,
148 listKeysOfPrefix 196
197 updateObjectACL,
198 updatePrefixACL,
199
200 listKeysOfPrefix,
201 createObjectReadStream
149} 202}
150 203
151// --------------------------------------------------------------------------- 204// ---------------------------------------------------------------------------
@@ -154,17 +207,15 @@ async function uploadToStorage (options: {
154 content: ReadStream 207 content: ReadStream
155 objectStorageKey: string 208 objectStorageKey: string
156 bucketInfo: BucketInfo 209 bucketInfo: BucketInfo
210 isPrivate: boolean
157}) { 211}) {
158 const { content, objectStorageKey, bucketInfo } = options 212 const { content, objectStorageKey, bucketInfo, isPrivate } = options
159 213
160 const input: PutObjectCommandInput = { 214 const input: PutObjectCommandInput = {
161 Body: content, 215 Body: content,
162 Bucket: bucketInfo.BUCKET_NAME, 216 Bucket: bucketInfo.BUCKET_NAME,
163 Key: buildKey(objectStorageKey, bucketInfo) 217 Key: buildKey(objectStorageKey, bucketInfo),
164 } 218 ACL: getACL(isPrivate)
165
166 if (CONFIG.OBJECT_STORAGE.UPLOAD_ACL) {
167 input.ACL = CONFIG.OBJECT_STORAGE.UPLOAD_ACL
168 } 219 }
169 220
170 const parallelUploads3 = new Upload({ 221 const parallelUploads3 = new Upload({
@@ -194,5 +245,50 @@ async function uploadToStorage (options: {
194 bucketInfo.PREFIX, objectStorageKey, bucketInfo.BUCKET_NAME, lTags() 245 bucketInfo.PREFIX, objectStorageKey, bucketInfo.BUCKET_NAME, lTags()
195 ) 246 )
196 247
197 return getPrivateUrl(bucketInfo, objectStorageKey) 248 return getInternalUrl(bucketInfo, objectStorageKey)
249}
250
251async function applyOnPrefix (options: {
252 prefix: string
253 bucketInfo: BucketInfo
254 commandBuilder: (obj: _Object) => Parameters<S3Client['send']>[0]
255
256 continuationToken?: string
257}) {
258 const { prefix, bucketInfo, commandBuilder, continuationToken } = options
259
260 const s3Client = getClient()
261
262 const commandPrefix = bucketInfo.PREFIX + prefix
263 const listCommand = new ListObjectsV2Command({
264 Bucket: bucketInfo.BUCKET_NAME,
265 Prefix: commandPrefix,
266 ContinuationToken: continuationToken
267 })
268
269 const listedObjects = await s3Client.send(listCommand)
270
271 if (isArray(listedObjects.Contents) !== true) {
272 const message = `Cannot apply function on ${commandPrefix} prefix in bucket ${bucketInfo.BUCKET_NAME}: no files listed.`
273
274 logger.error(message, { response: listedObjects, ...lTags() })
275 throw new Error(message)
276 }
277
278 for (const object of listedObjects.Contents) {
279 const command = commandBuilder(object)
280
281 await s3Client.send(command)
282 }
283
284 // Repeat if not all objects could be listed at once (limit of 1000?)
285 if (listedObjects.IsTruncated) {
286 await applyOnPrefix({ ...options, continuationToken: listedObjects.ContinuationToken })
287 }
288}
289
290function getACL (isPrivate: boolean) {
291 return isPrivate
292 ? CONFIG.OBJECT_STORAGE.UPLOAD_ACL.PRIVATE
293 : CONFIG.OBJECT_STORAGE.UPLOAD_ACL.PUBLIC
198} 294}
diff --git a/server/lib/object-storage/urls.ts b/server/lib/object-storage/urls.ts
index 2a889190b..a47a98b98 100644
--- a/server/lib/object-storage/urls.ts
+++ b/server/lib/object-storage/urls.ts
@@ -1,10 +1,14 @@
1import { CONFIG } from '@server/initializers/config' 1import { CONFIG } from '@server/initializers/config'
2import { OBJECT_STORAGE_PROXY_PATHS, WEBSERVER } from '@server/initializers/constants'
3import { MVideoUUID } from '@server/types/models'
2import { BucketInfo, buildKey, getEndpointParsed } from './shared' 4import { BucketInfo, buildKey, getEndpointParsed } from './shared'
3 5
4function getPrivateUrl (config: BucketInfo, keyWithoutPrefix: string) { 6function getInternalUrl (config: BucketInfo, keyWithoutPrefix: string) {
5 return getBaseUrl(config) + buildKey(keyWithoutPrefix, config) 7 return getBaseUrl(config) + buildKey(keyWithoutPrefix, config)
6} 8}
7 9
10// ---------------------------------------------------------------------------
11
8function getWebTorrentPublicFileUrl (fileUrl: string) { 12function getWebTorrentPublicFileUrl (fileUrl: string) {
9 const baseUrl = CONFIG.OBJECT_STORAGE.VIDEOS.BASE_URL 13 const baseUrl = CONFIG.OBJECT_STORAGE.VIDEOS.BASE_URL
10 if (!baseUrl) return fileUrl 14 if (!baseUrl) return fileUrl
@@ -19,11 +23,28 @@ function getHLSPublicFileUrl (fileUrl: string) {
19 return replaceByBaseUrl(fileUrl, baseUrl) 23 return replaceByBaseUrl(fileUrl, baseUrl)
20} 24}
21 25
26// ---------------------------------------------------------------------------
27
28function getHLSPrivateFileUrl (video: MVideoUUID, filename: string) {
29 return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}`
30}
31
32function getWebTorrentPrivateFileUrl (filename: string) {
33 return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + filename
34}
35
36// ---------------------------------------------------------------------------
37
22export { 38export {
23 getPrivateUrl, 39 getInternalUrl,
40
24 getWebTorrentPublicFileUrl, 41 getWebTorrentPublicFileUrl,
25 replaceByBaseUrl, 42 getHLSPublicFileUrl,
26 getHLSPublicFileUrl 43
44 getHLSPrivateFileUrl,
45 getWebTorrentPrivateFileUrl,
46
47 replaceByBaseUrl
27} 48}
28 49
29// --------------------------------------------------------------------------- 50// ---------------------------------------------------------------------------
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts
index e323baaa2..003807826 100644
--- a/server/lib/object-storage/videos.ts
+++ b/server/lib/object-storage/videos.ts
@@ -5,7 +5,17 @@ import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/model
5import { getHLSDirectory } from '../paths' 5import { getHLSDirectory } from '../paths'
6import { VideoPathManager } from '../video-path-manager' 6import { VideoPathManager } from '../video-path-manager'
7import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' 7import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys'
8import { listKeysOfPrefix, lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' 8import {
9 createObjectReadStream,
10 listKeysOfPrefix,
11 lTags,
12 makeAvailable,
13 removeObject,
14 removePrefix,
15 storeObject,
16 updateObjectACL,
17 updatePrefixACL
18} from './shared'
9 19
10function listHLSFileKeysOf (playlist: MStreamingPlaylistVideo) { 20function listHLSFileKeysOf (playlist: MStreamingPlaylistVideo) {
11 return listKeysOfPrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS) 21 return listKeysOfPrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
@@ -17,7 +27,8 @@ function storeHLSFileFromFilename (playlist: MStreamingPlaylistVideo, filename:
17 return storeObject({ 27 return storeObject({
18 inputPath: join(getHLSDirectory(playlist.Video), filename), 28 inputPath: join(getHLSDirectory(playlist.Video), filename),
19 objectStorageKey: generateHLSObjectStorageKey(playlist, filename), 29 objectStorageKey: generateHLSObjectStorageKey(playlist, filename),
20 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS 30 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
31 isPrivate: playlist.Video.hasPrivateStaticPath()
21 }) 32 })
22} 33}
23 34
@@ -25,7 +36,8 @@ function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string)
25 return storeObject({ 36 return storeObject({
26 inputPath: path, 37 inputPath: path,
27 objectStorageKey: generateHLSObjectStorageKey(playlist, basename(path)), 38 objectStorageKey: generateHLSObjectStorageKey(playlist, basename(path)),
28 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS 39 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
40 isPrivate: playlist.Video.hasPrivateStaticPath()
29 }) 41 })
30} 42}
31 43
@@ -35,7 +47,26 @@ function storeWebTorrentFile (video: MVideo, file: MVideoFile) {
35 return storeObject({ 47 return storeObject({
36 inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), 48 inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file),
37 objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), 49 objectStorageKey: generateWebTorrentObjectStorageKey(file.filename),
38 bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS 50 bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS,
51 isPrivate: video.hasPrivateStaticPath()
52 })
53}
54
55// ---------------------------------------------------------------------------
56
57function updateWebTorrentFileACL (video: MVideo, file: MVideoFile) {
58 return updateObjectACL({
59 objectStorageKey: generateWebTorrentObjectStorageKey(file.filename),
60 bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS,
61 isPrivate: video.hasPrivateStaticPath()
62 })
63}
64
65function updateHLSFilesACL (playlist: MStreamingPlaylistVideo) {
66 return updatePrefixACL({
67 prefix: generateHLSObjectBaseStorageKey(playlist),
68 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
69 isPrivate: playlist.Video.hasPrivateStaticPath()
39 }) 70 })
40} 71}
41 72
@@ -87,6 +118,39 @@ async function makeWebTorrentFileAvailable (filename: string, destination: strin
87 118
88// --------------------------------------------------------------------------- 119// ---------------------------------------------------------------------------
89 120
121function getWebTorrentFileReadStream (options: {
122 filename: string
123 rangeHeader: string
124}) {
125 const { filename, rangeHeader } = options
126
127 const key = generateWebTorrentObjectStorageKey(filename)
128
129 return createObjectReadStream({
130 key,
131 bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS,
132 rangeHeader
133 })
134}
135
136function getHLSFileReadStream (options: {
137 playlist: MStreamingPlaylistVideo
138 filename: string
139 rangeHeader: string
140}) {
141 const { playlist, filename, rangeHeader } = options
142
143 const key = generateHLSObjectStorageKey(playlist, filename)
144
145 return createObjectReadStream({
146 key,
147 bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
148 rangeHeader
149 })
150}
151
152// ---------------------------------------------------------------------------
153
90export { 154export {
91 listHLSFileKeysOf, 155 listHLSFileKeysOf,
92 156
@@ -94,10 +158,16 @@ export {
94 storeHLSFileFromFilename, 158 storeHLSFileFromFilename,
95 storeHLSFileFromPath, 159 storeHLSFileFromPath,
96 160
161 updateWebTorrentFileACL,
162 updateHLSFilesACL,
163
97 removeHLSObjectStorage, 164 removeHLSObjectStorage,
98 removeHLSFileObjectStorage, 165 removeHLSFileObjectStorage,
99 removeWebTorrentObjectStorage, 166 removeWebTorrentObjectStorage,
100 167
101 makeWebTorrentFileAvailable, 168 makeWebTorrentFileAvailable,
102 makeHLSFileAvailable 169 makeHLSFileAvailable,
170
171 getWebTorrentFileReadStream,
172 getHLSFileReadStream
103} 173}