3131import org .springframework .stereotype .Service ;
3232import org .springframework .transaction .annotation .Transactional ;
3333import org .springframework .web .client .HttpClientErrorException ;
34+ import software .amazon .awssdk .core .sync .ResponseTransformer ;
35+ import software .amazon .awssdk .services .s3 .S3Client ;
36+ import software .amazon .awssdk .services .s3 .model .DeleteObjectRequest ;
37+ import software .amazon .awssdk .services .s3 .model .GetObjectRequest ;
38+ import software .amazon .awssdk .services .s3 .model .PutObjectRequest ;
39+ import software .amazon .awssdk .services .s3 .presigner .S3Presigner ;
40+ import software .amazon .awssdk .services .s3 .presigner .model .PresignedPutObjectRequest ;
41+ import software .amazon .awssdk .services .s3 .presigner .model .PutObjectPresignRequest ;
3442
3543import java .io .BufferedInputStream ;
3644import java .io .IOException ;
4048import java .nio .file .Files ;
4149import java .nio .file .Path ;
4250import java .nio .file .StandardCopyOption ;
51+ import java .time .Duration ;
4352import java .util .ArrayList ;
4453import java .util .Enumeration ;
4554import java .util .List ;
4655import java .util .Optional ;
4756import java .util .Set ;
57+ import java .util .UUID ;
4858import java .util .zip .ZipEntry ;
4959import java .util .zip .ZipFile ;
5060
@@ -67,6 +77,55 @@ public class ModService {
6777 private final ModRepository modRepository ;
6878 private final ModVersionRepository modVersionRepository ;
6979 private final LicenseRepository licenseRepository ;
80+ private final S3Client s3Client ;
81+ private final S3Presigner s3Presigner ;
82+
83+ private String getBucketKey (int userId , UUID requestId ) {
84+ return "%s-mod-%s" .formatted (userId , requestId );
85+ }
86+
87+ public String getPresignedS3Url (Player uploader , UUID requestId ) {
88+ log .info ("User {} requested presigned url for mod upload, request id {}" , uploader .getId (), requestId );
89+
90+ checkUploaderVaultBan (uploader );
91+
92+ PutObjectRequest putObjectRequest = PutObjectRequest .builder ()
93+ .bucket (properties .getS3 ().getUserUploadBucket ())
94+ .key (getBucketKey (uploader .getId (), requestId ))
95+ .build ();
96+
97+ PutObjectPresignRequest putObjectPresignRequest = PutObjectPresignRequest .builder ()
98+ .signatureDuration (Duration .ofHours (1 ))
99+ .putObjectRequest (putObjectRequest )
100+ .build ();
101+
102+ PresignedPutObjectRequest presignedRequest = s3Presigner .presignPutObject (putObjectPresignRequest );
103+
104+ return presignedRequest .url ().toString ();
105+ }
106+
107+ public Path getModFromS3Location (Player uploader , UUID requestId ) throws IOException {
108+ Path tempDir = Files .createTempDirectory ("mod-download" );
109+ String bucketKey = getBucketKey (uploader .getId (), requestId );
110+ Path tempFile = tempDir .resolve (bucketKey + ".zip" );
111+
112+ checkUploaderVaultBan (uploader );
113+
114+ GetObjectRequest request = GetObjectRequest .builder ()
115+ .bucket (properties .getS3 ().getUserUploadBucket ())
116+ .key (bucketKey )
117+ .build ();
118+
119+ s3Client .getObject (request , ResponseTransformer .toFile (tempFile ));
120+
121+ return tempFile ;
122+ }
123+
124+ public void deleteModFromS3Location (Player uploader , UUID requestId ) throws IOException {
125+ String bucketKey = getBucketKey (uploader .getId (), requestId );
126+
127+ s3Client .deleteObject (DeleteObjectRequest .builder ().bucket (properties .getS3 ().getUserUploadBucket ()).key (bucketKey ).build ());
128+ }
70129
71130 @ SneakyThrows
72131 @ Transactional
@@ -94,8 +153,7 @@ public void processUploadedMod(Path uploadedFile, String originalFilename, Playe
94153 short version = (short ) Integer .parseInt (modInfo .getVersion ().toString ());
95154
96155 if (!canUploadMod (displayName , uploader )) {
97- Mod mod = modRepository .findOneByDisplayName (displayName )
98- .orElseThrow (() -> new IllegalStateException ("Mod could not be found" ));
156+ Mod mod = modRepository .findOneByDisplayName (displayName ).orElseThrow (() -> new IllegalStateException ("Mod could not be found" ));
99157 throw new ApiException (new Error (ErrorCode .MOD_NOT_ORIGINAL_AUTHOR , mod .getAuthor (), displayName ));
100158 }
101159
@@ -159,9 +217,7 @@ private void validateZipFileSafety(Path uploadedFile) {
159217
160218 try {
161219 // Unzipping directory already invokes the checks we want to perform
162- Unzipper .from (uploadedFile )
163- .to (tempDirectory )
164- .unzip ();
220+ Unzipper .from (uploadedFile ).to (tempDirectory ).unzip ();
165221
166222 } finally {
167223 log .debug ("Delete unzipped files in folder {}" , tempDirectory );
@@ -188,11 +244,7 @@ private void validateModStructure(Path uploadedFile) {
188244 }
189245
190246 private boolean modExists (String displayName , short version ) {
191- ModVersion probe = new ModVersion ()
192- .setVersion (version )
193- .setMod (new Mod ()
194- .setDisplayName (displayName )
195- );
247+ ModVersion probe = new ModVersion ().setVersion (version ).setMod (new Mod ().setDisplayName (displayName ));
196248
197249 return modVersionRepository .exists (Example .of (probe , ExampleMatcher .matching ()));
198250 }
@@ -202,14 +254,10 @@ private boolean modUidExists(String uuid) {
202254 }
203255
204256 private void checkUploaderVaultBan (Player uploader ) {
205- uploader .getActiveBanOf (BanLevel .VAULT )
206- .ifPresent ((banInfo ) -> {
207- String message = banInfo .getDuration () == BanDurationType .PERMANENT ?
208- "You are permanently banned from uploading mods to the vault." :
209- format ("You are banned from uploading mods to the vault until {0}." , banInfo .getExpiresAt ());
210- throw HttpClientErrorException .create (message , HttpStatus .FORBIDDEN , "Upload forbidden" ,
211- HttpHeaders .EMPTY , null , null );
212- });
257+ uploader .getActiveBanOf (BanLevel .VAULT ).ifPresent ((banInfo ) -> {
258+ String message = banInfo .getDuration () == BanDurationType .PERMANENT ? "You are permanently banned from uploading mods to the vault." : format ("You are banned from uploading mods to the vault until {0}." , banInfo .getExpiresAt ());
259+ throw HttpClientErrorException .create (message , HttpStatus .FORBIDDEN , "Upload forbidden" , HttpHeaders .EMPTY , null , null );
260+ });
213261 }
214262
215263 private boolean canUploadMod (String displayName , Player uploader ) {
@@ -248,7 +296,7 @@ private void validateModInfo(com.faforever.commons.mod.Mod modInfo) {
248296 final Integer versionInt = Ints .tryParse (modVersion .toString ());
249297 if (versionInt == null ) {
250298 errors .add (new Error (ErrorCode .MOD_VERSION_NOT_A_NUMBER , modVersion .toString ()));
251- } else if (!isModVersionValidRange (versionInt )){
299+ } else if (!isModVersionValidRange (versionInt )) {
252300 errors .add (new Error (ErrorCode .MOD_VERSION_INVALID_RANGE , MOD_VERSION_MIN_VALUE , MOD_VERSION_MAX_VALUE ));
253301 }
254302 }
@@ -307,22 +355,10 @@ private String generateFolderName(String displayName, short version) {
307355 }
308356
309357 private void store (com .faforever .commons .mod .Mod modInfo , Optional <Path > thumbnailPath , Player uploader , String zipFileName , Integer licenseId , String repositoryUrl ) {
310- ModVersion modVersion = new ModVersion ()
311- .setUid (modInfo .getUid ())
312- .setType (modInfo .isUiOnly () ? ModType .UI : ModType .SIM )
313- .setDescription (modInfo .getDescription ())
314- .setVersion ((short ) Integer .parseInt (modInfo .getVersion ().toString ()))
315- .setFilename (MOD_PATH_PREFIX + zipFileName )
316- .setIcon (thumbnailPath .map (path -> path .getFileName ().toString ()).orElse (null ));
358+ ModVersion modVersion = new ModVersion ().setUid (modInfo .getUid ()).setType (modInfo .isUiOnly () ? ModType .UI : ModType .SIM ).setDescription (modInfo .getDescription ()).setVersion ((short ) Integer .parseInt (modInfo .getVersion ().toString ())).setFilename (MOD_PATH_PREFIX + zipFileName ).setIcon (thumbnailPath .map (path -> path .getFileName ().toString ()).orElse (null ));
317359
318360 License newLicense = getLicenseOrDefault (licenseId );
319- Mod mod = modRepository .findOneByDisplayName (modInfo .getName ())
320- .orElse (new Mod ()
321- .setAuthor (modInfo .getAuthor ())
322- .setDisplayName (modInfo .getName ())
323- .setUploader (uploader )
324- .setLicense (newLicense )
325- .setRecommended (false ));
361+ Mod mod = modRepository .findOneByDisplayName (modInfo .getName ()).orElse (new Mod ().setAuthor (modInfo .getAuthor ()).setDisplayName (modInfo .getName ()).setUploader (uploader ).setLicense (newLicense ).setRecommended (false ));
326362
327363 if (newLicense .isLessPermissiveThan (mod .getLicense ())) {
328364 throw ApiException .of (ErrorCode .LESS_PERMISSIVE_LICENSE );
@@ -336,8 +372,6 @@ private void store(com.faforever.commons.mod.Mod modInfo, Optional<Path> thumbna
336372 }
337373
338374 public License getLicenseOrDefault (Integer licenseId ) {
339- return Optional .ofNullable (licenseId )
340- .flatMap (licenseRepository ::findById )
341- .orElseGet (() -> licenseRepository .getReferenceById (properties .getMod ().getDefaultLicenseId ()));
375+ return Optional .ofNullable (licenseId ).flatMap (licenseRepository ::findById ).orElseGet (() -> licenseRepository .getReferenceById (properties .getMod ().getDefaultLicenseId ()));
342376 }
343377}
0 commit comments