mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-07 15:31:00 +08:00
parent
a7c5a2f3cb
commit
0c71864347
@ -13,19 +13,23 @@ import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
import io.papermc.hangar.model.common.NamedPermission;
|
||||
import io.papermc.hangar.model.common.PermissionType;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import io.papermc.hangar.model.internal.versions.VersionUpload;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.security.annotations.permission.PermissionRequired;
|
||||
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
|
||||
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
|
||||
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
|
||||
import io.papermc.hangar.service.api.VersionsApiService;
|
||||
import io.papermc.hangar.service.internal.versions.DownloadService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Anyone
|
||||
@Controller
|
||||
@ -42,6 +46,14 @@ public class VersionsController implements IVersionsController {
|
||||
this.versionsApiService = versionsApiService;
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Override
|
||||
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 5)
|
||||
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.CREATE_VERSION, args = "{#author, #slug}")
|
||||
public void uploadVersion(final String author, final String slug, final List<MultipartFile> files, final VersionUpload versionUpload) {
|
||||
this.versionsApiService.uploadVersion(author, slug, files, versionUpload);
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibilityRequired(type = VisibilityRequired.Type.PROJECT, args = "{#author, #slug}")
|
||||
public Version getVersion(final String author, final String slug, final String name) {
|
||||
|
@ -5,6 +5,7 @@ import io.papermc.hangar.model.api.project.version.Version;
|
||||
import io.papermc.hangar.model.api.project.version.VersionStats;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import io.papermc.hangar.model.internal.versions.VersionUpload;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@ -12,34 +13,42 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Tag(name = "Versions")
|
||||
@RequestMapping(path = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public interface IVersionsController {
|
||||
|
||||
// TODO implement version creation via API
|
||||
/*@Operation(
|
||||
@Operation(
|
||||
summary ="Creates a new version",
|
||||
operationId = "deployVersion",
|
||||
notes = "Creates a new version for a project. Requires the `create_version` permission in the project or owning organization.",
|
||||
operationId = "uploadVersion",
|
||||
description = "Creates a new version for a project. Requires the `create_version` permission in the project or owning organization.",
|
||||
security = @SecurityRequirement(name = "Session"),
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = 201, description = "Ok"),
|
||||
@ApiResponse(responseCode = 401, description = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(responseCode = 403, description = "Not enough permissions to use this endpoint")
|
||||
@ApiResponse(responseCode = "201", description = "Ok"),
|
||||
@ApiResponse(responseCode = "401", description = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(responseCode = "403", description = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@PostMapping(path = "/projects/{author}/{slug}/versions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
ResponseEntity<Version> deployVersion()*/
|
||||
@PostMapping(path = "/projects/{author}/{slug}/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
void uploadVersion(@Parameter(description = "The author of the project to return versions for") @PathVariable String author,
|
||||
@Parameter(description = "The slug of the project to return versions for") @PathVariable String slug,
|
||||
@Parameter(description = "The version files in order of selected platforms, if any") @RequestPart(required = false) @Size(max = 3, message = "version.new.error.invalidNumOfPlatforms") List<@Valid MultipartFile> files,
|
||||
@Parameter(description = "Version data") @RequestPart @Valid VersionUpload versionUpload);
|
||||
|
||||
@Operation(
|
||||
summary = "Returns a specific version of a project",
|
||||
|
@ -83,7 +83,7 @@ public class VersionController extends HangarComponent {
|
||||
@RequestPart @Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms") final List<@Valid MultipartFileOrUrl> data,
|
||||
@RequestPart @NotBlank final String channel) {
|
||||
// Use separate lists to hack around multipart form data limitations
|
||||
return ResponseEntity.ok(this.versionFactory.createPendingVersion(projectId, data, files, channel));
|
||||
return ResponseEntity.ok(this.versionFactory.createPendingVersion(projectId, data, files, channel, true));
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
|
@ -33,6 +33,9 @@ public interface ProjectChannelsDAO {
|
||||
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId AND name = :name AND color = :color")
|
||||
ProjectChannelTable getProjectChannel(long projectId, String name, @EnumByOrdinal Color color);
|
||||
|
||||
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId AND name = :name")
|
||||
ProjectChannelTable getProjectChannel(long projectId, String name);
|
||||
|
||||
@SqlQuery("SELECT * FROM project_channels WHERE id = :channelId")
|
||||
ProjectChannelTable getProjectChannel(long channelId);
|
||||
|
||||
|
@ -35,13 +35,12 @@ public class PendingVersion {
|
||||
// @el(root: String)
|
||||
@NotBlank(message = "version.new.error.channel.noName")
|
||||
private final @Validate(SpEL = "@validate.regex(#root, @hangarConfig.channels.nameRegex)", message = "channel.modal.error.invalidName") String channelName;
|
||||
@NotNull(message = "version.new.error.channel.noColor")
|
||||
private final Color channelColor;
|
||||
private final Set<ChannelFlag> channelFlags;
|
||||
private final boolean forumSync;
|
||||
|
||||
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
|
||||
public PendingVersion(final String versionString, final Map<Platform, Set<PluginDependency>> pluginDependencies, final EnumMap<Platform, SortedSet<String>> platformDependencies, final String description, final List<PendingVersionFile> files, final String channelName, final Color channelColor, final Set<ChannelFlag> channelFlags, final boolean forumSync) {
|
||||
public PendingVersion(final String versionString, final Map<Platform, Set<PluginDependency>> pluginDependencies, final EnumMap<Platform, SortedSet<String>> platformDependencies, final String description, final List<PendingVersionFile> files, final String channelName, final @Nullable Color channelColor, final Set<ChannelFlag> channelFlags, final boolean forumSync) {
|
||||
this.versionString = versionString;
|
||||
this.pluginDependencies = pluginDependencies;
|
||||
this.platformDependencies = platformDependencies;
|
||||
@ -90,7 +89,7 @@ public class PendingVersion {
|
||||
return this.channelName;
|
||||
}
|
||||
|
||||
public Color getChannelColor() {
|
||||
public @Nullable Color getChannelColor() {
|
||||
return this.channelColor;
|
||||
}
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
package io.papermc.hangar.model.internal.versions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import java.util.List;
|
||||
|
||||
public class PlatformDependency {
|
||||
|
||||
private final Platform platform;
|
||||
private final List<String> versions;
|
||||
|
||||
@JsonCreator
|
||||
public PlatformDependency(final Platform platform, final List<String> versions) {
|
||||
this.platform = platform;
|
||||
this.versions = versions;
|
||||
}
|
||||
|
||||
public Platform getPlatform() {
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
public List<String> getVersions() {
|
||||
return this.versions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PlatformDependency{" +
|
||||
"platform=" + this.platform +
|
||||
", versions=" + this.versions +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package io.papermc.hangar.model.internal.versions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.util.*;
|
||||
import io.papermc.hangar.controller.validations.Validate;
|
||||
import io.papermc.hangar.model.api.project.version.PluginDependency;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
|
||||
public class VersionUpload {
|
||||
|
||||
// @el(root: String)
|
||||
@NotBlank(message = "version.new.error.invalidVersionString")
|
||||
private final @Validate(SpEL = "@validate.regex(#root, @hangarConfig.projects.versionNameRegex)", message = "version.new.error.invalidVersionString") String versionString;
|
||||
private final Map<Platform, Set<@Valid PluginDependency>> pluginDependencies;
|
||||
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms")
|
||||
private final Map<Platform, @Size(min = 1, message = "version.edit.error.noPlatformVersions") SortedSet<@NotBlank(message = "version.new.error.invalidPlatformVersion") String>> platformDependencies;
|
||||
|
||||
// @el(root: String)
|
||||
@NotBlank(message = "version.new.error.noDescription")
|
||||
private final @Validate(SpEL = "@validate.max(#root, @hangarConfig.pages.maxLen)", message = "page.new.error.maxLength") String description;
|
||||
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms")
|
||||
private final List<@Valid MultipartFileOrUrl> files;
|
||||
|
||||
// @el(root: String)
|
||||
@NotBlank(message = "version.new.error.channel.noName")
|
||||
private final @Validate(SpEL = "@validate.regex(#root, @hangarConfig.channels.nameRegex)", message = "channel.modal.error.invalidName") String channelName;
|
||||
|
||||
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
|
||||
public VersionUpload(final String versionString, final Map<Platform, Set<PluginDependency>> pluginDependencies, final EnumMap<Platform, SortedSet<String>> platformDependencies, final String description, final List<MultipartFileOrUrl> files, final String channelName) {
|
||||
this.versionString = versionString;
|
||||
this.pluginDependencies = pluginDependencies;
|
||||
this.platformDependencies = platformDependencies;
|
||||
this.description = description;
|
||||
this.files = files;
|
||||
this.channelName = channelName;
|
||||
}
|
||||
|
||||
public String getVersionString() {
|
||||
return this.versionString;
|
||||
}
|
||||
|
||||
public Map<Platform, Set<PluginDependency>> getPluginDependencies() {
|
||||
return this.pluginDependencies;
|
||||
}
|
||||
|
||||
public Map<Platform, SortedSet<String>> getPlatformDependencies() {
|
||||
return this.platformDependencies;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public List<MultipartFileOrUrl> getFiles() {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return this.channelName;
|
||||
}
|
||||
|
||||
public PendingVersion toPendingVersion(final List<PendingVersionFile> files) {
|
||||
return new PendingVersion(this.versionString,
|
||||
this.pluginDependencies,
|
||||
(EnumMap<Platform, SortedSet<String>>) this.platformDependencies,
|
||||
this.description,
|
||||
files,
|
||||
this.channelName,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VersionUpload{" +
|
||||
"versionString='" + this.versionString + '\'' +
|
||||
", pluginDependencies=" + this.pluginDependencies +
|
||||
", platformDependencies=" + this.platformDependencies +
|
||||
", description='" + this.description + '\'' +
|
||||
", files=" + this.files +
|
||||
", channelName='" + this.channelName + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -5,30 +5,52 @@ import io.papermc.hangar.db.dao.v1.VersionsApiDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.PaginatedResult;
|
||||
import io.papermc.hangar.model.api.Pagination;
|
||||
import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.version.Version;
|
||||
import io.papermc.hangar.model.api.project.version.VersionStats;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import io.papermc.hangar.model.internal.versions.PendingVersion;
|
||||
import io.papermc.hangar.model.internal.versions.VersionUpload;
|
||||
import io.papermc.hangar.service.internal.versions.VersionDependencyService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import io.papermc.hangar.service.internal.versions.VersionFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
public class VersionsApiService extends HangarComponent {
|
||||
|
||||
private final VersionsApiDAO versionsApiDAO;
|
||||
private final VersionDependencyService versionDependencyService;
|
||||
private final VersionFactory versionFactory;
|
||||
private final ProjectsApiService projectsApiService;
|
||||
|
||||
@Autowired
|
||||
public VersionsApiService(final VersionsApiDAO versionsApiDAO, final VersionDependencyService versionDependencyService) {
|
||||
public VersionsApiService(final VersionsApiDAO versionsApiDAO, final VersionDependencyService versionDependencyService, final VersionFactory versionFactory, final ProjectsApiService projectsApiService) {
|
||||
this.versionsApiDAO = versionsApiDAO;
|
||||
this.versionDependencyService = versionDependencyService;
|
||||
this.versionFactory = versionFactory;
|
||||
this.projectsApiService = projectsApiService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void uploadVersion(final String author, final String slug, final List<MultipartFile> files, final VersionUpload versionUpload) {
|
||||
final Project project = this.projectsApiService.getProject(author, slug);
|
||||
if (project == null) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// TODO Do the upload in one step
|
||||
final PendingVersion preparedPendingVersion = this.versionFactory.createPendingVersion(project.getId(), versionUpload.getFiles(), files, versionUpload.getChannelName(), false);
|
||||
final PendingVersion pendingVersion = versionUpload.toPendingVersion(preparedPendingVersion.getFiles());
|
||||
this.versionFactory.publishPendingVersion(project.getId(), pendingVersion);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
@ -145,6 +145,10 @@ public class ChannelService extends HangarComponent {
|
||||
return this.projectChannelsDAO.getProjectChannel(projectId, name, color);
|
||||
}
|
||||
|
||||
public ProjectChannelTable getProjectChannel(final long projectId, final String name) {
|
||||
return this.projectChannelsDAO.getProjectChannel(projectId, name);
|
||||
}
|
||||
|
||||
public ProjectChannelTable getProjectChannel(final long channelId) {
|
||||
return this.projectChannelsDAO.getProjectChannel(channelId);
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ public class VersionFactory extends HangarComponent {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PendingVersion createPendingVersion(final long projectId, final List<MultipartFileOrUrl> data, final List<MultipartFile> files, final String channel) {
|
||||
public PendingVersion createPendingVersion(final long projectId, final List<MultipartFileOrUrl> data, final List<MultipartFile> files, final String channel, final boolean prefillDependencies) {
|
||||
final ProjectTable projectTable = this.projectService.getProjectTable(projectId);
|
||||
if (projectTable == null) {
|
||||
throw new IllegalArgumentException();
|
||||
@ -135,9 +135,12 @@ public class VersionFactory extends HangarComponent {
|
||||
|
||||
if (fileOrUrl.isUrl()) {
|
||||
// Handle external url
|
||||
this.createPendingUrl(channel, projectTable, pluginDependencies, platformDependencies, pendingFiles, fileOrUrl);
|
||||
pendingFiles.add(new PendingVersionFile(fileOrUrl.platforms(), null, fileOrUrl.externalUrl()));
|
||||
if (prefillDependencies) {
|
||||
this.createPendingUrl(channel, projectTable, pluginDependencies, platformDependencies, fileOrUrl);
|
||||
}
|
||||
} else {
|
||||
versionString = this.createPendingFile(files.remove(0), channel, projectTable, pluginDependencies, platformDependencies, pendingFiles, versionString, userTempDir, fileOrUrl);
|
||||
versionString = this.createPendingFile(files.remove(0), channel, projectTable, pluginDependencies, platformDependencies, pendingFiles, versionString, userTempDir, fileOrUrl, prefillDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +151,9 @@ public class VersionFactory extends HangarComponent {
|
||||
return new PendingVersion(versionString, pluginDependencies, platformDependencies, pendingFiles, projectTable.isForumSync());
|
||||
}
|
||||
|
||||
private String createPendingFile(final MultipartFile file, final String channel, final ProjectTable projectTable, final Map<Platform, Set<PluginDependency>> pluginDependencies, final Map<Platform, SortedSet<String>> platformDependencies, final List<PendingVersionFile> pendingFiles, String versionString, final String userTempDir, final MultipartFileOrUrl fileOrUrl) {
|
||||
private String createPendingFile(final MultipartFile file, final String channel, final ProjectTable projectTable, final Map<Platform, Set<PluginDependency>> pluginDependencies,
|
||||
final Map<Platform, SortedSet<String>> platformDependencies, final List<PendingVersionFile> pendingFiles, String versionString,
|
||||
final String userTempDir, final MultipartFileOrUrl fileOrUrl, final boolean prefillDependencies) {
|
||||
// check extension
|
||||
final String pluginFileName = file.getOriginalFilename();
|
||||
if (pluginFileName == null || (!pluginFileName.endsWith(".zip") && !pluginFileName.endsWith(".jar"))) {
|
||||
@ -186,6 +191,7 @@ public class VersionFactory extends HangarComponent {
|
||||
pendingFiles.add(new PendingVersionFile(fileOrUrl.platforms(), fileInfo, null));
|
||||
|
||||
// setup dependencies
|
||||
if (prefillDependencies) {
|
||||
for (final Platform platform : fileOrUrl.platforms()) {
|
||||
final LastDependencies lastDependencies = this.getLastVersionDependencies(projectTable.getOwnerName(), projectTable.getSlug(), channel, platform);
|
||||
if (lastDependencies != null) {
|
||||
@ -205,11 +211,11 @@ public class VersionFactory extends HangarComponent {
|
||||
platformDependencies.put(platform, loadedPlatformDependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
return versionString;
|
||||
}
|
||||
|
||||
private void createPendingUrl(final String channel, final ProjectTable projectTable, final Map<Platform, Set<PluginDependency>> pluginDependencies, final Map<Platform, SortedSet<String>> platformDependencies, final List<PendingVersionFile> pendingFiles, final MultipartFileOrUrl fileOrUrl) {
|
||||
pendingFiles.add(new PendingVersionFile(fileOrUrl.platforms(), null, fileOrUrl.externalUrl()));
|
||||
private void createPendingUrl(final String channel, final ProjectTable projectTable, final Map<Platform, Set<PluginDependency>> pluginDependencies, final Map<Platform, SortedSet<String>> platformDependencies, final MultipartFileOrUrl fileOrUrl) {
|
||||
for (final Platform platform : fileOrUrl.platforms()) {
|
||||
final LastDependencies lastDependencies = this.getLastVersionDependencies(projectTable.getOwnerName(), projectTable.getSlug(), channel, platform);
|
||||
if (lastDependencies != null) {
|
||||
@ -244,8 +250,12 @@ public class VersionFactory extends HangarComponent {
|
||||
final String versionDir = this.projectFiles.getVersionDir(projectTable.getOwnerName(), projectTable.getSlug(), pendingVersion.getVersionString());
|
||||
try {
|
||||
// find channel
|
||||
ProjectChannelTable projectChannelTable = this.channelService.getProjectChannel(projectId, pendingVersion.getChannelName(), pendingVersion.getChannelColor());
|
||||
ProjectChannelTable projectChannelTable = this.channelService.getProjectChannel(projectId, pendingVersion.getChannelName());
|
||||
if (projectChannelTable == null) {
|
||||
if (pendingVersion.getChannelColor() == null) {
|
||||
throw new HangarApiException("version.new.error.channel.noColor");
|
||||
}
|
||||
|
||||
projectChannelTable = this.channelService.createProjectChannel(pendingVersion.getChannelName(), pendingVersion.getChannelColor(), projectId, pendingVersion.getChannelFlags().stream().filter(ChannelFlag::isEditable).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user