Skip to content

Commit

Permalink
Merge 011eae8 into f127590
Browse files Browse the repository at this point in the history
  • Loading branch information
norraxx committed Sep 24, 2019
2 parents f127590 + 011eae8 commit 3de0cd1
Show file tree
Hide file tree
Showing 16 changed files with 578 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.faforever.api.deployment;

import com.faforever.api.AbstractIntegrationTest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.jdbc.Sql;

import java.nio.file.Files;
import java.nio.file.Paths;

import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepFeaturedMods.sql")
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, scripts = "classpath:sql/cleanFeaturedMods.sql")
public class ExeUploadControllerTest extends AbstractIntegrationTest {
private MockMultipartFile file;
private static final String SUPER_SECRET = "banana";

@Before
public void setUp() {
super.setUp();
file = new MockMultipartFile("file", "ForgedAlliance.exe", "application/octet-stream", new byte[]{ 1, 2 ,3, 4 });
}

@Test
public void testSuccessUploadBeta() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("modName", "fafbeta")
.param("apiKey", SUPER_SECRET)
).andExpect(status().isOk());
assertTrue(Files.exists(Paths.get("build/exe/beta/ForgedAlliance.3706.exe")));
}

@Test
public void testSuccessUploadDevelop() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("modName", "fafdevelop")
.param("apiKey", SUPER_SECRET)
).andExpect(status().isOk());
assertTrue(Files.exists(Paths.get("build/exe/develop/ForgedAlliance.3707.exe")));
}

@Test
public void testBadRequestUploadNoModName() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("apiKey", SUPER_SECRET)
).andExpect(status().is4xxClientError());
}

@Test
public void testBadRequestUploadNoFile() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.param("modName", "fafdevelop")
.param("apiKey", SUPER_SECRET)
).andExpect(status().is4xxClientError());
}

@Test
public void testBadRequestUploadFileWithWrongExeExtension() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "ForgedAlliance.zip", "application/octet-stream", new byte[]{ 1, 2 ,3, 4 });
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("modName", "fafbeta")
.param("apiKey", SUPER_SECRET)
).andExpect(status().is4xxClientError());
}

@Test
public void testBadRequestUploadWithoutApiKey() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("modName", "fafbeta")
).andExpect(status().is4xxClientError());
}

@Test
public void testBadRequestUploadWithWrongApiKey() throws Exception {
this.mockMvc.perform(fileUpload("/exe/upload")
.file(file)
.param("modName", "fafbeta")
.param("apiKey", "not a banana")
).andExpect(status().is4xxClientError());
}
}
9 changes: 9 additions & 0 deletions src/inttest/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ faf-api:
mautic:
client-id: banana
client-secret: banana
deployment:
featured-mods-target-directory: "build/cache/mods"
repositories-directory: ""
forged-alliance-exe-path: "build/exe/faf"
testing-exe-upload-key: banana
forged-alliance-beta-exe-path: build/exe/beta
forged-alliance-develop-exe-path: build/exe/develop
featured-mod:
file-url-format: "file://%s/%s"


logging:
Expand Down
5 changes: 5 additions & 0 deletions src/inttest/resources/sql/cleanFeaturedMods.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DELETE FROM updates_fafdevelop_files;
DELETE FROM updates_fafbeta_files;
DELETE FROM updates_fafdevelop;
DELETE FROM updates_fafbeta;
DELETE FROM game_featuredMods;
25 changes: 25 additions & 0 deletions src/inttest/resources/sql/prepFeaturedMods.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
DELETE FROM updates_fafdevelop_files;
DELETE FROM updates_fafdevelop;
DELETE FROM updates_fafbeta_files;
DELETE FROM updates_fafbeta;
DELETE FROM reported_user;
DELETE FROM moderation_report;
DELETE FROM game_stats;
DELETE FROM game_featuredMods;

INSERT INTO `game_featuredMods` (`id`, `gamemod`, `description`, `name`, `publish`, `order`, `git_url`, `git_branch`, `file_extension`, `allow_override`, `deployment_webhook`) VALUES
(1, 'faf', 'Forged Alliance Forever', 'FAF', 1, 0, 'https://github.com/FAForever/fa.git', 'deploy/faf', 'nx2', 0, NULL),
(27, 'fafbeta', 'Beta version of the next FAF patch', 'FAF Beta', 1, 2, 'https://github.com/FAForever/fa.git', 'deploy/fafbeta', 'nx4', 1, NULL),
(28, 'fafdevelop', 'Updated frequently for testing the upcoming game Patch', 'FAF Develop', 1, 11, 'https://github.com/FAForever/fa.git', 'deploy/fafdevelop', 'nx5', 1, NULL);

INSERT INTO `updates_fafbeta` (`id`, `filename`, `path`) VALUES
(1, 'ForgedAlliance.exe', 'bin');

INSERT INTO `updates_fafbeta_files` (`id`, `fileId`, `version`, `name`, `md5`, `obselete`) VALUES
(1703, 1, 3706, 'ForgedAlliance.3706.exe', 'c20b922a785cf5876c39b7696a16f162', 0);

INSERT INTO `updates_fafdevelop` (`id`, `filename`, `path`) VALUES
(1, 'ForgedAlliance.exe', 'bin');

INSERT INTO `updates_fafdevelop_files` (`id`, `fileId`, `version`, `name`, `md5`, `obselete`) VALUES
(4327, 1, 3707, 'ForgedAlliance.3707.exe', '79f0ea70625ab464d369721183e9fd29', 0);
4 changes: 4 additions & 0 deletions src/main/java/com/faforever/api/config/FafApiProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ public static class Deployment {
private String repositoriesDirectory;
private String filesDirectoryFormat = "updates_%s_files";
private String forgedAllianceExePath;
private String testingExeUploadKey;
private String allowedExeExtension = "exe";
private String forgedAllianceBetaExePath;
private String forgedAllianceDevelopExePath;
}

@Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private Predicate<String> paths() {
regex("/users/.*"),
regex("/mods/.*"),
regex("/maps/.*"),
regex("/exe/.*"),
regex("/leaderboards/.*"),
regex("/voting/.*"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.faforever.api.deployment;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.google.common.io.Files;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE;

@RestController
@RequestMapping(path = "/exe")
@Slf4j
public class ExeUploaderController {
private final FafApiProperties apiProperties;
private final ExeUploaderService exeUploaderService;

public ExeUploaderController(
FafApiProperties apiProperties,
ExeUploaderService exeUploaderService
) {
this.apiProperties = apiProperties;
this.exeUploaderService = exeUploaderService;
}

@ApiOperation("Upload an exe file")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Success"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 500, message = "Failure")})
@RequestMapping(path = "/upload", method = RequestMethod.POST, produces = APPLICATION_JSON_UTF8_VALUE)
public void upload(@RequestParam("file") MultipartFile file,
@RequestParam("modName") String modName,
@RequestParam("apiKey") String apiKey
) throws Exception {
if (!apiKey.equals(apiProperties.getDeployment().getTestingExeUploadKey())) {
throw new ApiException(new Error(ErrorCode.API_KEY_INVALID));
}
String extension = Files.getFileExtension(file.getOriginalFilename());
if (!apiProperties.getDeployment().getAllowedExeExtension().equals(extension)) {
throw new ApiException(
new Error(ErrorCode.UPLOAD_INVALID_FILE_EXTENSIONS, apiProperties.getDeployment().getAllowedExeExtension())
);
}

log.info("Uploading exe file '{}' to '{}' directory", file.getOriginalFilename(), modName);

exeUploaderService.processUpload(file.getInputStream(), modName);
}
}
101 changes: 101 additions & 0 deletions src/main/java/com/faforever/api/deployment/ExeUploaderService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.faforever.api.deployment;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.content.ContentService;
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.faforever.api.featuredmods.FeaturedModFile;
import com.faforever.api.featuredmods.FeaturedModService;
import com.faforever.api.utils.FilePermissionUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.List;

import static com.google.common.hash.Hashing.md5;
import static com.google.common.io.Files.asByteSource;
import static java.nio.file.Files.createDirectories;

@Service
@Slf4j
public class ExeUploaderService {
private final ContentService contentService;
private final FafApiProperties apiProperties;
private final FeaturedModService featuredModService;

public ExeUploaderService(
ContentService contentService,
FafApiProperties apiProperties,
FeaturedModService featuredModService
) {
this.contentService = contentService;
this.apiProperties = apiProperties;
this.featuredModService = featuredModService;
}

@Transactional
@SneakyThrows
public void processUpload(InputStream exeDataInputStream, String modName) {
checkAllowedBranchName(modName);
FeaturedModFile featuredModFile = featuredModService.getFile(modName, null, "ForgedAlliance.exe");
featuredModFile.setName(String.format("ForgedAlliance.%d.exe", featuredModFile.getVersion()));
Path uploadedFile = this.upload(
exeDataInputStream,
featuredModFile.getName(),
modName
);
featuredModFile.setMd5(asByteSource(uploadedFile.toFile()).hash(md5()).toString());
exeDataInputStream.close();
List<FeaturedModFile> featuredModFiles = Collections.singletonList(featuredModFile);
featuredModService.save(modName, (short) featuredModFile.getVersion(), featuredModFiles);
}

private Path upload(InputStream exeDataInputStream, String fileName, String modName) throws IOException {
Assert.isTrue(exeDataInputStream.available() > 0, "data of 'ForgedAlliance.exe' must not be empty");

Path tempDir = contentService.createTempDir();
Path temporaryFile = tempDir.resolve(fileName);
Files.copy(exeDataInputStream, temporaryFile);

Path copyTo = getCopyToPath(modName, fileName);
createDirectories(copyTo.getParent(), FilePermissionUtil.directoryPermissionFileAttributes());
return Files.copy(
temporaryFile,
copyTo,
StandardCopyOption.REPLACE_EXISTING
);
}

private void checkAllowedBranchName(String modName) throws ApiException {
if (!"fafbeta".equals(modName) && !"fafdevelop".equals(modName)) {
throw new ApiException(new Error(ErrorCode.INVALID_FEATURED_MOD, modName));
}
}

private Path getCopyToPath(String modName, String fileName) {
String copyTo = null;
switch (modName) {
case "fafbeta":
copyTo = apiProperties.getDeployment().getForgedAllianceBetaExePath();
break;
case "fafdevelop":
copyTo = apiProperties.getDeployment().getForgedAllianceDevelopExePath();
break;
default:
throw new ApiException(new Error(ErrorCode.INVALID_FEATURED_MOD, modName));
}

return Paths.get(copyTo, fileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,9 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -108,7 +99,10 @@ public void run() {
Deployment deployment = apiProperties.getDeployment();
Path targetFolder = Paths.get(deployment.getFeaturedModsTargetDirectory(), String.format(deployment.getFilesDirectoryFormat(), modName));
List<StagedFile> files = packageDirectories(repositoryDirectory, version, fileIds, targetFolder);
createPatchedExe(version, fileIds, targetFolder).ifPresent(files::add);

if ("faf".equals(modName)) {
createPatchedExe(version, fileIds, targetFolder).ifPresent(files::add);
}

if (files.isEmpty()) {
log.warn("Could not find any files to deploy. Is the configuration correct?");
Expand Down Expand Up @@ -161,7 +155,7 @@ private Optional<StagedFile> createPatchedExe(short version, Map<String, Short>
}

private short readModVersion(Path modPath) {
return (short) Integer.parseInt(new ModReader().readDirectory(modPath).getVersion().toString());
return Short.parseShort(new ModReader().readDirectory(modPath).getVersion().toString());
}

private void verifyVersion(int version, boolean allowOverride, String modName) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/faforever/api/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ public enum ErrorCode {
MAP_NAME_TOO_SHORT(187, "Map name invalid", "The map name must have at least {0, number} characters, was: {1, number}"),
MAP_NAME_DOES_NOT_START_WITH_LETTER(188, "Map name invalid", "The map name has to begin with a letter"),
PARSING_LUA_FILE_FAILED(189, "Parsing lua files failed", "During the parsing of the lua file an error occured: {0}"),
NO_RUSH_RADIUS_MISSING(190, "No rush radius missing", "The scenario file must specify a no rush radius");
NO_RUSH_RADIUS_MISSING(190, "No rush radius missing", "The scenario file must specify a no rush radius"),
INVALID_FEATURED_MOD(191, "Invalid featured mod name", "The featured mod name ''{0}'' is not allowed in this context."),
API_KEY_INVALID(192, "Api key is invalid", "The api key is invalid.");

private final int code;
private final String title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public FeaturedModService(FeaturedModRepository featuredModRepository, LegacyFea
this.legacyFeaturedModFileRepository = legacyFeaturedModFileRepository;
}

public FeaturedModFile getFile(String modName, Integer version, String fileName) {
return legacyFeaturedModFileRepository.getFile(modName, version, fileName);
}

@Cacheable(FEATURED_MOD_FILES_CACHE_NAME)
public List<FeaturedModFile> getFiles(String modName, @Nullable Integer version) {
return legacyFeaturedModFileRepository.getFiles(modName, version);
Expand Down
Loading

0 comments on commit 3de0cd1

Please sign in to comment.