@@ -2,22 +2,19 @@ import {
22 encodeFileName ,
33 getOriginalFileName ,
44} from 'omniboxd/utils/encode-filename' ;
5- import { Injectable , Logger , HttpStatus } from '@nestjs/common' ;
6- import { AppException } from 'omniboxd/common/exceptions/app.exception' ;
7- import { I18nService } from 'nestjs-i18n' ;
5+ import { Injectable , Logger } from '@nestjs/common' ;
86import { Response } from 'express' ;
9- import { S3Service } from 'omniboxd/s3/s3.service' ;
7+ import { ObjectMeta , S3Service } from 'omniboxd/s3/s3.service' ;
108import { PermissionsService } from 'omniboxd/permissions/permissions.service' ;
119import { ResourcePermission } from 'omniboxd/permissions/resource-permission.enum' ;
12- import { objectStreamResponse } from 'omniboxd/s3/utils' ;
1310import { ResourceAttachmentsService } from 'omniboxd/resource-attachments/resource-attachments.service' ;
1411import {
1512 UploadAttachmentsResponseDto ,
1613 UploadedAttachmentDto ,
1714} from './dto/upload-attachments-response.dto' ;
18- import { SharesService } from 'omniboxd/shares/shares.service' ;
1915import { SharedResourcesService } from 'omniboxd/shared-resources/shared-resources.service' ;
2016import { Share } from 'omniboxd/shares/entities/share.entity' ;
17+ import { Readable } from 'stream' ;
2118
2219@Injectable ( )
2320export class AttachmentsService {
@@ -27,31 +24,53 @@ export class AttachmentsService {
2724 private readonly s3Service : S3Service ,
2825 private readonly permissionsService : PermissionsService ,
2926 private readonly resourceAttachmentsService : ResourceAttachmentsService ,
30- private readonly sharesService : SharesService ,
3127 private readonly sharedResourcesService : SharedResourcesService ,
32- private readonly i18n : I18nService ,
3328 ) { }
3429
35- async checkPermission (
36- namespaceId : string ,
37- resourceId : string ,
38- userId : string ,
39- permission : ResourcePermission = ResourcePermission . CAN_VIEW ,
40- ) {
41- const hasPermission = await this . permissionsService . userHasPermission (
42- namespaceId ,
43- resourceId ,
44- userId ,
45- permission ,
46- ) ;
47- if ( ! hasPermission ) {
48- const message = this . i18n . t ( 'auth.errors.notAuthorized' ) ;
49- throw new AppException ( message , 'NOT_AUTHORIZED' , HttpStatus . FORBIDDEN ) ;
30+ private s3Path ( attachmentId : string ) : string {
31+ return `attachments/${ attachmentId } ` ;
32+ }
33+
34+ private isMedia ( mimetype ?: string ) : boolean {
35+ for ( const type of [ 'image/' , 'audio/' ] ) {
36+ if ( mimetype ?. startsWith ( type ) ) {
37+ return true ;
38+ }
5039 }
40+ return false ;
5141 }
5242
53- s3Path ( attachmentId : string ) : string {
54- return `attachments/${ attachmentId } ` ;
43+ private objectStreamResponse (
44+ objectStream : Readable ,
45+ objectMeta : ObjectMeta ,
46+ httpResponse : Response ,
47+ cacheControl : boolean = true ,
48+ forceDownload : boolean = true ,
49+ ) {
50+ const headers : Record < string , string > = { } ;
51+ if ( objectMeta . metadata ?. filename ) {
52+ const disposition = forceDownload ? 'attachment' : 'inline' ;
53+ headers [ 'Content-Disposition' ] =
54+ `${ disposition } ; filename*=UTF-8''${ encodeURIComponent ( objectMeta . metadata . filename ) } ` ;
55+ }
56+ if ( objectMeta . contentType ) {
57+ headers [ 'Content-Type' ] = objectMeta . contentType ;
58+ }
59+ if ( objectMeta . contentLength ) {
60+ headers [ 'Content-Length' ] = objectMeta . contentLength . toString ( ) ;
61+ }
62+ if ( objectMeta . lastModified ) {
63+ headers [ 'Last-Modified' ] = objectMeta . lastModified . toUTCString ( ) ;
64+ }
65+ if ( cacheControl ) {
66+ headers [ 'Cache-Control' ] = 'public, max-age=31536000' ; // 1 year
67+ } else {
68+ headers [ 'Cache-Control' ] = 'no-cache, no-store, must-revalidate' ;
69+ }
70+ for ( const [ key , value ] of Object . entries ( headers ) ) {
71+ httpResponse . setHeader ( key , value ) ;
72+ }
73+ objectStream . pipe ( httpResponse ) ;
5574 }
5675
5776 async uploadAttachment (
@@ -62,7 +81,7 @@ export class AttachmentsService {
6281 buffer : Buffer ,
6382 mimetype : string ,
6483 ) {
65- await this . checkPermission (
84+ await this . permissionsService . userHasPermissionOrFail (
6685 namespaceId ,
6786 resourceId ,
6887 userId ,
@@ -89,7 +108,7 @@ export class AttachmentsService {
89108 userId : string ,
90109 files : Express . Multer . File [ ] ,
91110 ) : Promise < UploadAttachmentsResponseDto > {
92- await this . checkPermission (
111+ await this . permissionsService . userHasPermissionOrFail (
93112 namespaceId ,
94113 resourceId ,
95114 userId ,
@@ -135,7 +154,7 @@ export class AttachmentsService {
135154 userId : string ,
136155 httpResponse : Response ,
137156 ) {
138- await this . checkPermission (
157+ await this . permissionsService . userHasPermissionOrFail (
139158 namespaceId ,
140159 resourceId ,
141160 userId ,
@@ -148,16 +167,11 @@ export class AttachmentsService {
148167 attachmentId ,
149168 ) ;
150169
151- const objectResponse = await this . s3Service . get (
170+ const { stream , meta } = await this . s3Service . getObject (
152171 this . s3Path ( attachmentId ) ,
153172 ) ;
154-
155- // Display media files inline, download other files as attachments
156- const forceDownload = ! this . isMedia ( objectResponse . mimetype ) ;
157-
158- return objectStreamResponse ( objectResponse , httpResponse , {
159- forceDownload,
160- } ) ;
173+ const forceDownload = ! this . isMedia ( meta . contentType ) ;
174+ this . objectStreamResponse ( stream , meta , httpResponse , true , forceDownload ) ;
161175 }
162176
163177 async deleteAttachment (
@@ -166,7 +180,7 @@ export class AttachmentsService {
166180 attachmentId : string ,
167181 userId : string ,
168182 ) {
169- await this . checkPermission (
183+ await this . permissionsService . userHasPermissionOrFail (
170184 namespaceId ,
171185 resourceId ,
172186 userId ,
@@ -197,25 +211,10 @@ export class AttachmentsService {
197211 resourceId ,
198212 attachmentId ,
199213 ) ;
200-
201- const objectResponse = await this . s3Service . get (
214+ const { stream, meta } = await this . s3Service . getObject (
202215 this . s3Path ( attachmentId ) ,
203216 ) ;
204-
205- // Display media files inline, download other files as attachments
206- const forceDownload = ! this . isMedia ( objectResponse . mimetype ) ;
207-
208- return objectStreamResponse ( objectResponse , httpResponse , {
209- forceDownload,
210- } ) ;
211- }
212-
213- isMedia ( mimetype : string ) : boolean {
214- for ( const type of [ 'image/' , 'audio/' ] ) {
215- if ( mimetype . startsWith ( type ) ) {
216- return true ;
217- }
218- }
219- return false ;
217+ const forceDownload = ! this . isMedia ( meta . contentType ) ;
218+ this . objectStreamResponse ( stream , meta , httpResponse , true , forceDownload ) ;
220219 }
221220}
0 commit comments