@@ -3,13 +3,20 @@ import {
3
3
PutObjectCommand as PutObject ,
4
4
} from '@aws-sdk/client-s3' ;
5
5
import { Type } from '@nestjs/common' ;
6
+ import { bufferFromStream } from '@seedcompany/common' ;
6
7
import { Command } from '@smithy/smithy-client' ;
7
8
import { pickBy } from 'lodash' ;
8
9
import { DateTime , Duration } from 'luxon' ;
9
10
import { URL } from 'node:url' ;
11
+ import { Readable } from 'stream' ;
10
12
import { assert } from 'ts-essentials' ;
11
- import { InputException } from '~/common' ;
12
- import { FileBucket , GetObjectOutput , SignedOp } from './file-bucket' ;
13
+ import {
14
+ FileBucket ,
15
+ GetObjectOutput ,
16
+ InvalidSignedUrlException ,
17
+ PutObjectInput ,
18
+ SignedOp ,
19
+ } from './file-bucket' ;
13
20
14
21
export interface LocalBucketOptions {
15
22
baseUrl : URL ;
@@ -62,12 +69,25 @@ export abstract class LocalBucket<
62
69
63
70
protected abstract saveFile ( key : string , file : FakeAwsFile ) : Promise < void > ;
64
71
72
+ async putObject ( input : PutObjectInput ) {
73
+ const buffer =
74
+ input . Body instanceof Readable
75
+ ? await bufferFromStream ( input . Body )
76
+ : Buffer . from ( input . Body ) ;
77
+ await this . saveFile ( input . Key , {
78
+ LastModified : new Date ( ) ,
79
+ ...input ,
80
+ Body : buffer ,
81
+ ContentLength : buffer . byteLength ,
82
+ } ) ;
83
+ }
84
+
65
85
async getSignedUrl < TCommandInput extends object > (
66
86
operation : Type < Command < TCommandInput , any , any > > ,
67
87
input : SignedOp < TCommandInput > ,
68
88
) {
69
89
const signed = JSON . stringify ( {
70
- operation : operation . constructor . name ,
90
+ operation : operation . name . replace ( / C o m m a n d $ / , '' ) ,
71
91
...input ,
72
92
signing : {
73
93
...input . signing ,
@@ -88,27 +108,40 @@ export abstract class LocalBucket<
88
108
operation : Type < Command < TCommandInput , any , any > > ,
89
109
url : string ,
90
110
) : SignedOp < TCommandInput > & { Key : string } {
91
- let raw ;
111
+ let u : URL ;
92
112
try {
93
- raw = new URL ( url ) . searchParams . get ( 'signed' ) ;
113
+ u = new URL ( url ) ;
94
114
} catch ( e ) {
95
- raw = url ;
115
+ u = new URL ( 'http://localhost' ) ;
116
+ u . searchParams . set ( 'signed' , url ) ;
96
117
}
97
- assert ( typeof raw === 'string' ) ;
98
- let parsed ;
99
118
try {
100
- parsed = JSON . parse ( raw ) as SignedOp < TCommandInput > & {
119
+ const parsed = this . parseSignedUrl ( u ) as SignedOp < TCommandInput > & {
101
120
operation : string ;
102
- Key : string ;
103
121
} ;
104
- assert ( parsed . operation === operation . constructor . name ) ;
122
+ assert (
123
+ parsed . operation === operation . name ||
124
+ `${ parsed . operation } Command` === operation . name ,
125
+ ) ;
126
+ return parsed ;
127
+ } catch ( e ) {
128
+ throw new InvalidSignedUrlException ( e ) ;
129
+ }
130
+ }
131
+
132
+ parseSignedUrl ( url : URL ) {
133
+ const raw = url . searchParams . get ( 'signed' ) ;
134
+ let parsed ;
135
+ try {
136
+ parsed = JSON . parse ( raw || '' ) as SignedOp < { operation : string } > ;
137
+ assert ( typeof parsed . operation === 'string' ) ;
105
138
assert ( typeof parsed . Key === 'string' ) ;
106
139
assert ( typeof parsed . signing . expiresIn === 'number' ) ;
107
140
} catch ( e ) {
108
- throw new InputException ( e ) ;
141
+ throw new InvalidSignedUrlException ( e ) ;
109
142
}
110
143
if ( DateTime . local ( ) > DateTime . fromMillis ( parsed . signing . expiresIn ) ) {
111
- throw new InputException ( 'url expired') ;
144
+ throw new InvalidSignedUrlException ( 'URL expired') ;
112
145
}
113
146
return parsed ;
114
147
}
0 commit comments