publish site to custom server
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.krrishg.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SchemaMigration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SchemaMigration.class);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public SchemaMigration(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void migrate() {
|
||||
try {
|
||||
jdbcTemplate.execute("ALTER TABLE self_hosted_configs DROP COLUMN IF EXISTS encrypted_ssh_key");
|
||||
log.info("Dropped deprecated column encrypted_ssh_key from self_hosted_configs");
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not drop encrypted_ssh_key column: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.krrishg.controller;
|
||||
|
||||
import com.krrishg.config.UserPrincipal;
|
||||
import com.krrishg.model.Site;
|
||||
import com.krrishg.repository.SiteRepository;
|
||||
import com.krrishg.service.DeploymentService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -14,15 +16,19 @@ import java.util.UUID;
|
||||
public class DeploymentController {
|
||||
|
||||
private final DeploymentService deploymentService;
|
||||
private final SiteRepository siteRepository;
|
||||
|
||||
public DeploymentController(DeploymentService deploymentService) {
|
||||
public DeploymentController(DeploymentService deploymentService,
|
||||
SiteRepository siteRepository) {
|
||||
this.deploymentService = deploymentService;
|
||||
this.siteRepository = siteRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/publish")
|
||||
public ResponseEntity<?> publish(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id) {
|
||||
findSiteForUser(id, principal.id());
|
||||
try {
|
||||
String url = deploymentService.publish(id);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
@@ -38,7 +44,17 @@ public class DeploymentController {
|
||||
public ResponseEntity<?> unpublish(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id) {
|
||||
findSiteForUser(id, principal.id());
|
||||
deploymentService.unpublish(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Site unpublished successfully"));
|
||||
}
|
||||
|
||||
private Site findSiteForUser(UUID siteId, UUID userId) {
|
||||
Site site = siteRepository.findById(siteId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
|
||||
if (!site.getUserId().equals(userId)) {
|
||||
throw new IllegalArgumentException("Site not found");
|
||||
}
|
||||
return site;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.krrishg.controller;
|
||||
|
||||
import com.krrishg.config.UserPrincipal;
|
||||
import com.krrishg.dto.SelfHostedConfigRequest;
|
||||
import com.krrishg.dto.SelfHostedConfigResponse;
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.model.Site;
|
||||
import com.krrishg.repository.SelfHostedConfigRepository;
|
||||
import com.krrishg.repository.SiteRepository;
|
||||
import com.krrishg.service.Deployer;
|
||||
import com.krrishg.service.KeyManager;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/sites/{id}/self-hosted")
|
||||
public class SelfHostedController {
|
||||
|
||||
private final SiteRepository siteRepository;
|
||||
private final SelfHostedConfigRepository selfHostedConfigRepository;
|
||||
private final Deployer deployer;
|
||||
private final KeyManager keyManager;
|
||||
|
||||
public SelfHostedController(SiteRepository siteRepository,
|
||||
SelfHostedConfigRepository selfHostedConfigRepository,
|
||||
Deployer deployer,
|
||||
KeyManager keyManager) {
|
||||
this.siteRepository = siteRepository;
|
||||
this.selfHostedConfigRepository = selfHostedConfigRepository;
|
||||
this.deployer = deployer;
|
||||
this.keyManager = keyManager;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> getConfig(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id) {
|
||||
findSiteForUser(id, principal.id());
|
||||
var config = selfHostedConfigRepository.findBySiteId(id);
|
||||
if (config.isPresent()) {
|
||||
String publicKey = keyManager.getPublicKey(id.toString());
|
||||
return ResponseEntity.ok(SelfHostedConfigResponse.from(config.get(), publicKey));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("configured", false));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PutMapping
|
||||
public ResponseEntity<?> saveConfig(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody SelfHostedConfigRequest request) {
|
||||
Site site = findSiteForUser(id, principal.id());
|
||||
|
||||
selfHostedConfigRepository.findBySiteId(id).ifPresent(existing ->
|
||||
selfHostedConfigRepository.delete(existing));
|
||||
|
||||
String deployPath = request.getDeployPath() != null && !request.getDeployPath().isBlank()
|
||||
? request.getDeployPath()
|
||||
: "/var/www/{domain}/public";
|
||||
|
||||
String nginxSitesPath = request.getNginxSitesPath() != null && !request.getNginxSitesPath().isBlank()
|
||||
? request.getNginxSitesPath()
|
||||
: "/etc/nginx/sites-available";
|
||||
|
||||
SelfHostedConfig config = SelfHostedConfig.builder()
|
||||
.siteId(id)
|
||||
.domain(request.getDomain())
|
||||
.sshHost(request.getSshHost())
|
||||
.sshPort(request.getSshPort())
|
||||
.sshUser(request.getSshUser())
|
||||
.deployPath(deployPath)
|
||||
.nginxSitesPath(nginxSitesPath)
|
||||
.build();
|
||||
|
||||
config = selfHostedConfigRepository.save(config);
|
||||
|
||||
String publicKey = keyManager.getPublicKey(id.toString());
|
||||
|
||||
return ResponseEntity.ok(SelfHostedConfigResponse.from(config, publicKey));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@DeleteMapping
|
||||
public ResponseEntity<?> removeConfig(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id) {
|
||||
Site site = findSiteForUser(id, principal.id());
|
||||
|
||||
SelfHostedConfig config = selfHostedConfigRepository.findBySiteId(id).orElse(null);
|
||||
if (config != null) {
|
||||
try {
|
||||
deployer.remove(config);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
selfHostedConfigRepository.delete(config);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("message", "Self-hosted configuration removed"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PostMapping("/deploy")
|
||||
public ResponseEntity<?> deploy(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED)
|
||||
.body(Map.of("error", "Use POST /api/sites/{id}/publish instead"));
|
||||
}
|
||||
|
||||
private Site findSiteForUser(UUID siteId, UUID userId) {
|
||||
Site site = siteRepository.findById(siteId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
|
||||
if (!site.getUserId().equals(userId)) {
|
||||
throw new IllegalArgumentException("Site not found");
|
||||
}
|
||||
return site;
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,14 @@ public class SiteController {
|
||||
throw new IllegalArgumentException("Subdomain already taken");
|
||||
}
|
||||
|
||||
Site.DeploymentTarget target = parseDeploymentTarget(request.getDeploymentTarget());
|
||||
|
||||
Site site = Site.builder()
|
||||
.userId(principal.id())
|
||||
.name(request.getName())
|
||||
.description(request.getDescription())
|
||||
.subdomain(subdomain)
|
||||
.deploymentTarget(target)
|
||||
.build();
|
||||
|
||||
site = siteRepository.save(site);
|
||||
@@ -61,8 +64,8 @@ public class SiteController {
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<SiteResponse> updateSite(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody SiteRequest request) {
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody SiteRequest request) {
|
||||
Site site = findSiteForUser(id, principal.id());
|
||||
|
||||
String newSubdomain = sanitizeSubdomain(request.getSubdomain());
|
||||
@@ -76,6 +79,10 @@ public class SiteController {
|
||||
site.setDescription(request.getDescription());
|
||||
site.setSubdomain(newSubdomain);
|
||||
|
||||
if (request.getDeploymentTarget() != null) {
|
||||
site.setDeploymentTarget(parseDeploymentTarget(request.getDeploymentTarget()));
|
||||
}
|
||||
|
||||
site = siteRepository.save(site);
|
||||
return ResponseEntity.ok(SiteResponse.from(site));
|
||||
}
|
||||
@@ -107,4 +114,15 @@ public class SiteController {
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private Site.DeploymentTarget parseDeploymentTarget(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return Site.DeploymentTarget.NONE;
|
||||
}
|
||||
try {
|
||||
return Site.DeploymentTarget.valueOf(value.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Site.DeploymentTarget.NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.krrishg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SelfHostedConfigRequest {
|
||||
|
||||
@NotBlank(message = "Domain is required")
|
||||
private String domain;
|
||||
|
||||
@NotBlank(message = "SSH host is required")
|
||||
private String sshHost;
|
||||
|
||||
private int sshPort = 22;
|
||||
|
||||
@NotBlank(message = "SSH user is required")
|
||||
private String sshUser;
|
||||
|
||||
private String deployPath;
|
||||
|
||||
private String nginxSitesPath;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.krrishg.dto;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SelfHostedConfigResponse {
|
||||
|
||||
private UUID id;
|
||||
private UUID siteId;
|
||||
private String domain;
|
||||
private String sshHost;
|
||||
private int sshPort;
|
||||
private String sshUser;
|
||||
private String deployPath;
|
||||
private String nginxSitesPath;
|
||||
private String publicKey;
|
||||
private LocalDateTime deployedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public static SelfHostedConfigResponse from(SelfHostedConfig config, String publicKey) {
|
||||
return SelfHostedConfigResponse.builder()
|
||||
.id(config.getId())
|
||||
.siteId(config.getSiteId())
|
||||
.domain(config.getDomain())
|
||||
.sshHost(config.getSshHost())
|
||||
.sshPort(config.getSshPort())
|
||||
.sshUser(config.getSshUser())
|
||||
.deployPath(config.getDeployPath())
|
||||
.nginxSitesPath(config.getNginxSitesPath())
|
||||
.publicKey(publicKey)
|
||||
.deployedAt(config.getDeployedAt())
|
||||
.createdAt(config.getCreatedAt())
|
||||
.updatedAt(config.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,6 @@ public class SiteRequest {
|
||||
private String description;
|
||||
|
||||
private String subdomain;
|
||||
|
||||
private String deploymentTarget;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class SiteResponse {
|
||||
private String subdomain;
|
||||
private String publishedUrl;
|
||||
private SiteStatus status;
|
||||
private String deploymentTarget;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
@@ -34,6 +35,7 @@ public class SiteResponse {
|
||||
.subdomain(site.getSubdomain())
|
||||
.publishedUrl(site.getPublishedUrl())
|
||||
.status(site.getStatus())
|
||||
.deploymentTarget(site.getDeploymentTarget().name())
|
||||
.publishedAt(site.getPublishedAt())
|
||||
.createdAt(site.getCreatedAt())
|
||||
.updatedAt(site.getUpdatedAt())
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.krrishg.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "self_hosted_configs")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SelfHostedConfig {
|
||||
|
||||
@Id
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private UUID siteId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String domain;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String sshHost;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private int sshPort = 22;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String sshUser;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private String deployPath = "/var/www/{domain}/public";
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private String nginxSitesPath = "/etc/nginx/sites-available";
|
||||
|
||||
private LocalDateTime deployedAt;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,14 @@ public class Site {
|
||||
@Builder.Default
|
||||
private SiteStatus status = SiteStatus.DRAFT;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Builder.Default
|
||||
private DeploymentTarget deploymentTarget = DeploymentTarget.NONE;
|
||||
|
||||
public DeploymentTarget getDeploymentTarget() {
|
||||
return deploymentTarget != null ? deploymentTarget : DeploymentTarget.NONE;
|
||||
}
|
||||
|
||||
private LocalDateTime publishedAt;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
@@ -47,6 +55,10 @@ public class Site {
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public enum DeploymentTarget {
|
||||
GIT_PAGES, SELF_HOSTED, NONE
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (id == null) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.krrishg.repository;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SelfHostedConfigRepository extends JpaRepository<SelfHostedConfig, UUID> {
|
||||
|
||||
Optional<SelfHostedConfig> findBySiteId(UUID siteId);
|
||||
|
||||
Optional<SelfHostedConfig> findByDomain(String domain);
|
||||
|
||||
boolean existsBySiteId(UUID siteId);
|
||||
|
||||
boolean existsByDomain(String domain);
|
||||
}
|
||||
13
backend/src/main/java/com/krrishg/service/Deployer.java
Normal file
13
backend/src/main/java/com/krrishg/service/Deployer.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.service.RenderService.SiteOutput;
|
||||
|
||||
public interface Deployer {
|
||||
|
||||
String deploy(SiteOutput output, SelfHostedConfig config) throws Exception;
|
||||
|
||||
void remove(SelfHostedConfig config) throws Exception;
|
||||
|
||||
String buildUrl(SelfHostedConfig config);
|
||||
}
|
||||
@@ -2,10 +2,14 @@ package com.krrishg.service;
|
||||
|
||||
import com.krrishg.dto.SiteStructure;
|
||||
import com.krrishg.model.GitRemote;
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.model.Site;
|
||||
import com.krrishg.model.Site.DeploymentTarget;
|
||||
import com.krrishg.model.SiteStatus;
|
||||
import com.krrishg.repository.GitRemoteRepository;
|
||||
import com.krrishg.repository.SelfHostedConfigRepository;
|
||||
import com.krrishg.repository.SiteRepository;
|
||||
import com.krrishg.service.RenderService.SiteOutput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -23,17 +27,23 @@ public class DeploymentService {
|
||||
private final SiteRepository siteRepository;
|
||||
private final LLMService llmService;
|
||||
private final GitRemoteRepository gitRemoteRepository;
|
||||
private final SelfHostedConfigRepository selfHostedConfigRepository;
|
||||
private final Deployer deployer;
|
||||
|
||||
public DeploymentService(RenderService renderService,
|
||||
GitService gitService,
|
||||
SiteRepository siteRepository,
|
||||
LLMService llmService,
|
||||
GitRemoteRepository gitRemoteRepository) {
|
||||
GitRemoteRepository gitRemoteRepository,
|
||||
SelfHostedConfigRepository selfHostedConfigRepository,
|
||||
Deployer deployer) {
|
||||
this.renderService = renderService;
|
||||
this.gitService = gitService;
|
||||
this.siteRepository = siteRepository;
|
||||
this.llmService = llmService;
|
||||
this.gitRemoteRepository = gitRemoteRepository;
|
||||
this.selfHostedConfigRepository = selfHostedConfigRepository;
|
||||
this.deployer = deployer;
|
||||
}
|
||||
|
||||
public String publish(UUID siteId) {
|
||||
@@ -45,19 +55,46 @@ public class DeploymentService {
|
||||
throw new IllegalStateException("No generation versions found for site " + siteId);
|
||||
}
|
||||
|
||||
SiteOutput output = renderService.renderSite(structure);
|
||||
|
||||
GitRemote remote = gitRemoteRepository.findBySiteId(siteId).orElse(null);
|
||||
if (remote == null) {
|
||||
throw new IllegalStateException("No git remote configured. Connect a GitHub or GitLab remote first.");
|
||||
if (remote != null) {
|
||||
gitService.deployToPages(siteId.toString(), remote, structure);
|
||||
}
|
||||
|
||||
String url = gitService.deployToPages(siteId.toString(), remote, structure);
|
||||
SelfHostedConfig selfHostedConfig = selfHostedConfigRepository.findBySiteId(siteId).orElse(null);
|
||||
if (selfHostedConfig != null) {
|
||||
try {
|
||||
deployer.deploy(output, selfHostedConfig);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to deploy to custom server: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
String url = switch (site.getDeploymentTarget()) {
|
||||
case GIT_PAGES -> {
|
||||
if (remote == null) {
|
||||
throw new IllegalStateException(
|
||||
"Deployment target is GIT_PAGES but no git remote is configured");
|
||||
}
|
||||
yield PagesProvider.forRemote(remote).buildPagesUrl(remote);
|
||||
}
|
||||
case SELF_HOSTED -> {
|
||||
if (selfHostedConfig == null) {
|
||||
throw new IllegalStateException(
|
||||
"Deployment target is SELF_HOSTED but no self-hosted config exists");
|
||||
}
|
||||
yield deployer.buildUrl(selfHostedConfig);
|
||||
}
|
||||
case NONE -> null;
|
||||
};
|
||||
|
||||
site.setStatus(SiteStatus.PUBLISHED);
|
||||
site.setPublishedAt(LocalDateTime.now());
|
||||
site.setPublishedUrl(url);
|
||||
siteRepository.save(site);
|
||||
|
||||
log.info("Published site {} to {} at {}", siteId, remote.getProvider(), url);
|
||||
log.info("Published site {}: target={}, url={}", siteId, site.getDeploymentTarget(), url);
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -65,7 +102,19 @@ public class DeploymentService {
|
||||
Site site = siteRepository.findById(siteId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
|
||||
|
||||
gitService.removePages(siteId.toString());
|
||||
GitRemote remote = gitRemoteRepository.findBySiteId(siteId).orElse(null);
|
||||
if (remote != null) {
|
||||
gitService.removePages(siteId.toString());
|
||||
}
|
||||
|
||||
SelfHostedConfig selfHostedConfig = selfHostedConfigRepository.findBySiteId(siteId).orElse(null);
|
||||
if (selfHostedConfig != null) {
|
||||
try {
|
||||
deployer.remove(selfHostedConfig);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to remove self-hosted deployment for site {}: {}", siteId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
site.setStatus(SiteStatus.DRAFT);
|
||||
site.setPublishedAt(null);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.model.Site;
|
||||
import com.krrishg.repository.SelfHostedConfigRepository;
|
||||
import com.krrishg.repository.SiteRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -15,31 +17,41 @@ public class DomainService {
|
||||
private static final Logger log = LoggerFactory.getLogger(DomainService.class);
|
||||
|
||||
private final SiteRepository siteRepository;
|
||||
private final SelfHostedConfigRepository selfHostedConfigRepository;
|
||||
private final String siteDomain;
|
||||
|
||||
public DomainService(SiteRepository siteRepository,
|
||||
SelfHostedConfigRepository selfHostedConfigRepository,
|
||||
@Value("${indie.domain}") String siteDomain) {
|
||||
this.siteRepository = siteRepository;
|
||||
this.selfHostedConfigRepository = selfHostedConfigRepository;
|
||||
this.siteDomain = siteDomain;
|
||||
}
|
||||
|
||||
public Optional<Site> resolveSite(String hostname) {
|
||||
String subdomain = extractSubdomain(hostname);
|
||||
if (hostname == null || hostname.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String lower = hostname.toLowerCase().strip();
|
||||
|
||||
Optional<SelfHostedConfig> config = selfHostedConfigRepository.findByDomain(lower);
|
||||
if (config.isPresent()) {
|
||||
return siteRepository.findById(config.get().getSiteId());
|
||||
}
|
||||
|
||||
String subdomain = extractSubdomain(lower);
|
||||
if (subdomain != null) {
|
||||
return siteRepository.findBySubdomain(subdomain);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private String extractSubdomain(String hostname) {
|
||||
if (hostname == null || hostname.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String lower = hostname.toLowerCase();
|
||||
String suffix = "." + siteDomain;
|
||||
if (lower.endsWith(suffix)) {
|
||||
String subdomain = lower.substring(0, lower.indexOf(suffix));
|
||||
if (hostname.endsWith(suffix)) {
|
||||
String subdomain = hostname.substring(0, hostname.indexOf(suffix));
|
||||
if (!subdomain.isEmpty() && !subdomain.contains(".")) {
|
||||
return subdomain;
|
||||
}
|
||||
|
||||
@@ -323,6 +323,10 @@ public class GitService {
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
String processOutput = new String(process.getInputStream().readAllBytes());
|
||||
if (processOutput.contains("nothing to commit")) {
|
||||
log.info("Nothing to commit, skipping: {}", cmd);
|
||||
continue;
|
||||
}
|
||||
throw new RuntimeException("git command failed (exit " + exitCode + "): " + cmd + "\n" + processOutput);
|
||||
}
|
||||
}
|
||||
|
||||
162
backend/src/main/java/com/krrishg/service/SshRsyncDeployer.java
Normal file
162
backend/src/main/java/com/krrishg/service/SshRsyncDeployer.java
Normal file
@@ -0,0 +1,162 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.SelfHostedConfig;
|
||||
import com.krrishg.service.RenderService.SiteOutput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class SshRsyncDeployer implements Deployer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SshRsyncDeployer.class);
|
||||
|
||||
private final KeyManager keyManager;
|
||||
|
||||
public SshRsyncDeployer(KeyManager keyManager) {
|
||||
this.keyManager = keyManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deploy(SiteOutput output, SelfHostedConfig config) throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("indie-deploy-" + config.getSiteId());
|
||||
try {
|
||||
String resolvedDeployPath = config.getDeployPath().replace("{domain}", config.getDomain());
|
||||
String resolvedNginxPath = config.getNginxSitesPath().replace("{domain}", config.getDomain());
|
||||
|
||||
writeSiteFiles(tempDir, output);
|
||||
|
||||
String keyFile = keyManager.getPrivateKeyPath(config.getSiteId().toString()).toString();
|
||||
|
||||
ssh(keyFile, config, "mkdir -p " + resolvedDeployPath);
|
||||
|
||||
rsync(tempDir.resolve("site"), keyFile, config, resolvedDeployPath);
|
||||
|
||||
String nginxConfig = buildNginxConfig(config.getDomain(), resolvedDeployPath);
|
||||
Path nginxConfPath = tempDir.resolve(config.getDomain());
|
||||
Files.writeString(nginxConfPath, nginxConfig);
|
||||
|
||||
String remoteTemp = "/tmp/" + config.getDomain();
|
||||
rsyncSingleFile(nginxConfPath, keyFile, config, "/tmp", config.getDomain());
|
||||
ssh(keyFile, config,
|
||||
"sudo cp " + remoteTemp + " " + resolvedNginxPath + "/" + config.getDomain()
|
||||
+ " && rm " + remoteTemp
|
||||
+ " && sudo ln -sf " + resolvedNginxPath + "/" + config.getDomain()
|
||||
+ " /etc/nginx/sites-enabled/ && sudo nginx -s reload");
|
||||
|
||||
log.info("Deployed site {} to {} ({}", config.getSiteId(), config.getDomain(), resolvedDeployPath);
|
||||
return buildUrl(config);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(SelfHostedConfig config) throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("indie-deploy-remove-" + config.getSiteId());
|
||||
try {
|
||||
String resolvedDeployPath = config.getDeployPath().replace("{domain}", config.getDomain());
|
||||
|
||||
String keyFile = keyManager.getPrivateKeyPath(config.getSiteId().toString()).toString();
|
||||
|
||||
ssh(keyFile, config, "rm -rf " + resolvedDeployPath);
|
||||
|
||||
String resolvedNginxPath = config.getNginxSitesPath().replace("{domain}", config.getDomain());
|
||||
ssh(keyFile, config,
|
||||
"sudo rm -f " + resolvedNginxPath + "/" + config.getDomain()
|
||||
+ " /etc/nginx/sites-enabled/" + config.getDomain()
|
||||
+ " && sudo nginx -s reload");
|
||||
|
||||
log.info("Removed deployment for site {} ({})", config.getSiteId(), config.getDomain());
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildUrl(SelfHostedConfig config) {
|
||||
return "https://" + config.getDomain() + "/";
|
||||
}
|
||||
|
||||
private void writeSiteFiles(Path tempDir, SiteOutput output) throws IOException {
|
||||
Path siteDir = tempDir.resolve("site");
|
||||
Files.createDirectories(siteDir);
|
||||
for (Map.Entry<String, byte[]> entry : output.asFileMap().entrySet()) {
|
||||
Path filePath = siteDir.resolve(entry.getKey());
|
||||
Files.createDirectories(filePath.getParent());
|
||||
Files.write(filePath, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void ssh(String keyFile, SelfHostedConfig config, String command) throws Exception {
|
||||
String sshCmd = String.format(
|
||||
"ssh -i %s -p %d -o StrictHostKeyChecking=accept-new %s@%s \"%s\"",
|
||||
keyFile, config.getSshPort(), config.getSshUser(), config.getSshHost(), command
|
||||
);
|
||||
execute(sshCmd);
|
||||
}
|
||||
|
||||
private void rsync(Path sourceDir, String keyFile, SelfHostedConfig config, String targetDir) throws Exception {
|
||||
String rsyncCmd = String.format(
|
||||
"rsync -avz --delete -e 'ssh -p %d -i %s -o StrictHostKeyChecking=accept-new' %s/ %s@%s:%s/",
|
||||
config.getSshPort(), keyFile, sourceDir.toString(),
|
||||
config.getSshUser(), config.getSshHost(), targetDir
|
||||
);
|
||||
execute(rsyncCmd);
|
||||
}
|
||||
|
||||
private void rsyncSingleFile(Path sourceFile, String keyFile, SelfHostedConfig config,
|
||||
String targetDir, String filename) throws Exception {
|
||||
ssh(keyFile, config, "mkdir -p " + targetDir);
|
||||
String rsyncCmd = String.format(
|
||||
"rsync -avz -e 'ssh -p %d -i %s -o StrictHostKeyChecking=accept-new' %s %s@%s:%s/%s",
|
||||
config.getSshPort(), keyFile, sourceFile.toString(),
|
||||
config.getSshUser(), config.getSshHost(), targetDir, filename
|
||||
);
|
||||
execute(rsyncCmd);
|
||||
}
|
||||
|
||||
private String buildNginxConfig(String domain, String rootPath) {
|
||||
return "server {\n"
|
||||
+ " listen 80;\n"
|
||||
+ " server_name " + domain + ";\n"
|
||||
+ " root " + rootPath + ";\n"
|
||||
+ " index index.html;\n"
|
||||
+ " location / {\n"
|
||||
+ " try_files $uri $uri/ /index.html;\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
private void execute(String command) throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder("sh", "-c", command);
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
String output = new String(process.getInputStream().readAllBytes());
|
||||
throw new RuntimeException("Command failed (exit " + exitCode + "): " + command + "\n" + output);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDirectory(Path path) throws IOException {
|
||||
if (Files.exists(path)) {
|
||||
try (var walk = Files.walk(path)) {
|
||||
walk.sorted(Comparator.reverseOrder())
|
||||
.forEach(p -> {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete {}", p);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user