# SPDX-License-Identifier: BSD-2-Clause # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileCopyrightText: 2023 Ben Cooksley # SPDX-FileContributor: Ingo Klöcker import dataclasses import logging import os import tarfile from pathlib import Path from tempfile import TemporaryDirectory from typing import Optional from sftpnotary import projects, util from sftpnotary.core import Job, Request, Response, TaskProcessor, Worker from sftpnotary.exceptions import Error from sftpnotary.sftp import SFTPClient, SFTPError log = logging.getLogger(__name__) @dataclasses.dataclass class PublishWebsiteRequest(Request): websiteArchiveFileName: Optional[str] = None def validate(self): util.validateFileName(self.websiteArchiveFileName, strict=True) PublishWebsiteResponse = Response class PublishWebsiteJob(Job): def __init__( self, sftp: SFTPClient, websiteArchiveFilePath: Path, token: str, projectPath: str, branch: str, ): super().__init__(sftp) self.websiteArchiveFilePath = websiteArchiveFilePath self.request = PublishWebsiteRequest( websiteArchiveFileName=None, token=token, projectPath=projectPath, branch=branch, ) self.resultPath = self.websiteArchiveFilePath.parent def start(self): websiteArchiveFileName = self.websiteArchiveFilePath.name self.sftp.upload(self.websiteArchiveFilePath, self.task.path() / websiteArchiveFileName) self.request.websiteArchiveFileName = websiteArchiveFileName self.task.putRequest(self.request) self.taskQueue.commitTask(self.task) def waitForCompletion(self): self.taskQueue.waitForCompletion(self.task) log.debug(f"Fetching response of task '{self.task.id}' ...") response = None try: response = self.task.fetchResponse(PublishWebsiteResponse) except SFTPError as e: log.error("Error: %s", e) if response is None: log.error("Error: Got no response. Task failed.") else: self.success = True self.downloadLogFiles(self.resultPath) self.taskQueue.removeTask(self.task) class PublishWebsiteProcessor(TaskProcessor): requestClass = PublishWebsiteRequest def getProjectSetting(self, name, required=True): value = projects.settings.get(self.request.projectPath, self.request.branch, name) if required and not value: raise Error( f"Project setting '{name}' not set for branch {self.request.branch} " f"of project {self.request.projectPath}" ) return value def doProcess(self): # Get the details for where we will publish the website to rsyncDestination = self.getProjectSetting("rsyncDestination") websiteDomain = self.getProjectSetting("domain") # Make sure the website domain we have been given is valid... if not websiteDomain: raise Error(f"Website domain has not been set for branch {self.request.branch} and is mandatory") with TemporaryDirectory() as workPathName: workPath = Path(workPathName) localWebsiteArchiveFilePath = workPath / self.request.websiteArchiveFileName self.sftp.download(self.task.path() / self.request.websiteArchiveFileName, localWebsiteArchiveFilePath) # Setup a path for us to unpack the website archive into... unpackedWebsitePath = workPath / "unpacked-site" os.makedirs(unpackedWebsitePath) # Unpack the website archive.... archive = tarfile.open(name=localWebsiteArchiveFilePath, mode="r") # Set "nosec B202" because of https://github.com/PyCQA/bandit/issues/1038#issuecomment-1635863856 archive.extractall(filter="tar", path=unpackedWebsitePath) # nosec B202 archive.close() # Determine the final rsync destination # We allow for the website domain to be optionally added into this via use of %s # This avoids having to repeat ourselves in the configuration file finalRsyncDestination = rsyncDestination % websiteDomain # Build up the rsync command to run then run it # The entire contents of rsyncCommand = [ "rsync", "--archive", "--checksum", "--delete", "--verbose", str(unpackedWebsitePath) + "/", finalRsyncDestination.rstrip("/") + "/", ] util.runCommand(rsyncCommand, cwd=workPathName) class PublishWebsiteWorker(Worker): processorClass = PublishWebsiteProcessor