Skip to content

Commit

Permalink
Merge 246a38c into d820ea2
Browse files Browse the repository at this point in the history
  • Loading branch information
norraxx committed Aug 25, 2019
2 parents d820ea2 + 246a38c commit 1e143e6
Show file tree
Hide file tree
Showing 18 changed files with 1,409 additions and 20 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ git:

env:
global:
- GRADLE_CLI_OPTS="--console=plain --stacktrace --quiet"
- GRADLE_CLI_OPTS="--console=plain --stacktrace --full-stacktrace"

before_install:
- sudo /etc/init.d/mysql stop
Expand All @@ -35,7 +35,7 @@ after_success:
- ./gradlew ${GRADLE_CLI_OPTS} jacocoTestReport coveralls
- export IMAGE_TAG=faf-java-api;
- export REPO=faforever/faf-java-api;
- if [ -n "${TRAVIS_TAG}" -o "${TRAVIS_BRANCH}" == "develop" ]; then
- if [ -n "${TRAVIS_TAG}" -o "${TRAVIS_BRANCH}" == "feature/#327-uploads" ]; then
docker build -t ${IMAGE_TAG} . &&
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin &&
docker tag ${IMAGE_TAG} ${REPO}:${DOCEKR_IMAGE_VERSION} &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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());
}
}
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 @@
DROP TABLE updates_fafdevelop_files;
DELETE FROM updates_fafbeta_files;
DROP TABLE updates_fafdevelop;
DELETE FROM updates_fafbeta;
DELETE FROM game_featuredMods;
886 changes: 886 additions & 0 deletions src/inttest/resources/sql/prepFeaturedMods.sql

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/inttest/resources/sql/prepGameData.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ VALUES
(25, 'coop', 'Coop', 'Multiplayer campaign games', 1, 'https://github.com/FAForever/fa-coop.git', 'master', 'cop',
TRUE);

-- INSERT INTO `game_validity` (`id`, `message`) VALUES (0, 'Valid') ON DUPLICATE KEY UPDATE `id` = VALUES(id);

INSERT INTO game_stats (id, startTime, gameName, gameType, gameMod, `host`, mapId, validity) VALUES
(1, NOW(), 'Test game', '0', 6, 1, 1, 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.getBytes(), modName);
}
}
95 changes: 95 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,95 @@
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.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.hash;
import static java.nio.file.Files.createDirectories;

/**
* remove this shit
*/
@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(byte[] bytes, 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(
bytes,
featuredModFile.getName(),
modName
);
featuredModFile.setMd5(hash(uploadedFile.toFile(), md5()).toString());

List<FeaturedModFile> featuredModFiles = Collections.singletonList(featuredModFile);
featuredModService.save(modName, (short) featuredModFile.getVersion(), featuredModFiles);
}

@SneakyThrows
public Path upload(byte[] exeData, String fileName, String modName) {
Assert.isTrue(exeData.length > 0, "data of 'ForgedAlliance.exe' must not be empty");

Path tempDir = contentService.createTempDir();
Path temporaryFile = tempDir.resolve(fileName);
Files.write(temporaryFile, exeData);

Path copyTo = Paths.get(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 String getCopyToPath(String modName, String fileName) {
String copyTo = apiProperties.getDeployment().getForgedAllianceBetaExePath();
if ("fafdevelop".equals(modName)) {
copyTo = apiProperties.getDeployment().getForgedAllianceDevelopExePath();
}
return 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 @@ -91,7 +91,9 @@ public enum ErrorCode {
VOTING_CHOICE_DOES_NOT_EXIST(181, "Invalid choice", "There is no voting choice with the ID ''{0}''."),
STEAM_ID_ALREADY_LINKED(182, " Steam account already linked to a FAF account", "You linked this account already to user with name ''{0}''."),
MAP_NAME_INVALID(183, "Map name invalid", "The name of the map in the scenario file can only contain printable ASCII characters and blanks."),
MOD_NAME_INVALID(184, "Mod name invalid", "The name of the mod in the scenario file can only contain printable ASCII characters and blanks.");
MOD_NAME_INVALID(184, "Mod name invalid", "The name of the mod in the scenario file can only contain printable ASCII characters and blanks."),
INVALID_FEATURED_MOD(185, "Invalid featured mod name", "The featured mod name ''{0}'' is not allowed in this context."),
API_KEY_INVALID(185, "Api key is invalid", "The api key is invalid.");


private final int code;
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 1e143e6

Please sign in to comment.