~jan0sch/smederee

Showing details for patch 4c4f84e46f317725351692477c09b62a47f660b7.
2022-09-17 (Sat), 5:25 PM - Jens Grassel - 4c4f84e46f317725351692477c09b62a47f660b7

VCS: Download current distribution archive.

- add download route
- create dist file
- download dist file
- very rough on the edges
Summary of changes
7 files modified with 142 lines added and 17 lines removed
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala with 21 added and 0 removed lines
  • modules/hub/src/main/resources/messages_en.properties with 2 added and 0 removed lines
  • modules/hub/src/main/resources/reference.conf with 4 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/HubServer.scala with 1 added and 3 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 103 added and 11 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala with 9 added and 3 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html with 2 added and 0 removed lines
diff -rN -u old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala
--- old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-02 10:07:39.682646743 +0000
@@ -98,6 +98,27 @@
     } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
   }
 
+  /** Create a distribution archive from the given darcs repository.
+    *
+    * @param basePath
+    *   The base path under which the repository is located.
+    * @param repositoryName
+    *   The name of the repository.
+    * @param options
+    *   Additional options for the initialize command.
+    * @return
+    *   The output of the darcs command.
+    */
+  def dist(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+    log.trace(s"Execute $darcsBinary dist for $repositoryName")
+    val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
+    val darcsOptions        = List("dist") ::: options.toList
+    val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
+    for {
+      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
+    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+  }
+
   /** Initialize a darcs repository under the given base path with the provided name. This is done by running
     * the external darcs binary with the appropriate parameters.
     *
diff -rN -u old-smederee/modules/hub/src/main/resources/messages_en.properties new-smederee/modules/hub/src/main/resources/messages_en.properties
--- old-smederee/modules/hub/src/main/resources/messages_en.properties	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/resources/messages_en.properties	2025-02-02 10:07:39.682646743 +0000
@@ -141,6 +141,8 @@
 repository.overview.clone.title=Clone this repository
 repository.overview.clone.read-only=read-only
 repository.overview.clone.read-write=read-write
+repository.overview.download.title=Download
+repository.overview.download.link=Download the current distribution tar file.
 repository.overview.latest-changes=Latest changes
 repository.overview.latest-changes.timestamp={0,date,yyyy-MM-dd (E)}, {0,time,short}
 
diff -rN -u old-smederee/modules/hub/src/main/resources/reference.conf new-smederee/modules/hub/src/main/resources/reference.conf
--- old-smederee/modules/hub/src/main/resources/reference.conf	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/resources/reference.conf	2025-02-02 10:07:39.682646743 +0000
@@ -26,6 +26,10 @@
   host = "localhost"
   # The TCP port number on which the service shall listen for requests.
   port = 8080
+  # A directory into which files are written that are supposed to be downloaded by users (e.g. distribution
+  # files of repositories).
+  download-directory = /var/tmp/smederee/download
+  download-directory = ${?SMEDEREE_DOWNLOAD_DIR}
 
   # Settings affecting how the service will communicate several information to
   # the "outside world" e.g. if it runs behind a reverse proxy.
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-02-02 10:07:39.682646743 +0000
@@ -267,6 +267,9 @@
   *   The hostname on which the service shall listen for requests.
   * @param port
   *   The TCP port number on which the service shall listen for requests.
+  * @param downloadDirectory
+  *   A directory into which files are written that are supposed to be downloaded by users (e.g. distribution
+  *   files of repositories).
   * @param authentication
   *   The configuration of the authentication feature.
   * @param billing
@@ -286,6 +289,7 @@
 final case class ServiceConfig(
     host: Host,
     port: Port,
+    downloadDirectory: DirectoryPath,
     authentication: AuthenticationConfiguration,
     billing: BillingConfiguration,
     darcs: DarcsConfiguration,
@@ -301,8 +305,9 @@
 
   given Eq[ServiceConfig] = Eq.fromUniversalEquals
 
-  given ConfigReader[Host] = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port] = ConfigReader.fromStringOpt[Port](Port.fromString)
+  given ConfigReader[Host]          = ConfigReader.fromStringOpt[Host](Host.fromString)
+  given ConfigReader[Port]          = ConfigReader.fromStringOpt[Port](Port.fromString)
+  given ConfigReader[DirectoryPath] = ConfigReader.fromStringOpt(DirectoryPath.fromString)
 
   given ConfigReader[EmailServerUsername] =
     ConfigReader.fromStringOpt[EmailServerUsername](EmailServerUsername.from)
@@ -317,9 +322,10 @@
     )
 
   given ConfigReader[ServiceConfig] =
-    ConfigReader.forProduct9(
+    ConfigReader.forProduct10(
       "host",
       "port",
+      "download-directory",
       AuthenticationConfiguration.parentKey.toString,
       BillingConfiguration.parentKey.toString,
       DarcsConfiguration.parentKey.toString,
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-02 10:07:39.682646743 +0000
@@ -119,10 +119,8 @@
       landingPages    = new LandingPageRoutes[IO](configuration.service.external)
       vcsMetadataRepo = new DoobieVcsMetadataRepository[IO](transactor)
       vcsRepoRoutes = new VcsRepositoryRoutes[IO](
-        configuration.service.darcs,
+        configuration.service,
         darcsWrapper,
-        configuration.service.external,
-        configuration.service.ssh,
         vcsMetadataRepo
       )
       protectedRoutesWithFallThrough = authenticationWithFallThrough(
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 10:07:39.682646743 +0000
@@ -43,24 +43,18 @@
 
 /** Routes for handling VCS repositories, including creation, management and public serving.
   *
-  * @param darcsConfig
-  *   The configuration for darcs related operations.
+  * @param configuration
+  *   The hub service configuration.
   * @param darcs
   *   A class providing darcs VCS operations.
-  * @param linkConfig
-  *   The configuration needed to build correct links which are working from the outside.
-  * @param sshConfig
-  *   Settings for the embedded SSH server component.
   * @param vcsMetadataRepo
   *   A repository for handling database operations regarding our vcs repositories and their metadata.
   * @tparam F
   *   A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync.
   */
 final class VcsRepositoryRoutes[F[_]: Async](
-    darcsConfig: DarcsConfiguration,
+    configuration: ServiceConfig,
     darcs: DarcsCommands[F],
-    linkConfig: ExternalLinkConfig,
-    sshConfig: SshServerConfiguration,
     vcsMetadataRepo: VcsMetadataRepository[F]
 ) extends Http4sDsl[F] {
   private val log = LoggerFactory.getLogger(getClass)
@@ -68,9 +62,79 @@
   private val createRepoPath  = uri"/repo/create"
   private val MaximumFileSize = 131072L // TODO Move to configuration directive.
 
+  val darcsConfig = configuration.darcs
+  val linkConfig  = configuration.external
+  val sshConfig   = configuration.ssh
+
   // The base URI for our site which that be passed into some templates which create links themselfes.
   private val baseUri = linkConfig.createFullUri(Uri())
 
+  /** Logic for creating a distribution file and serving it as a download to the user.
+    *
+    * @param csrf
+    *   An optional CSRF-Token that shall be used.
+    * @param user
+    *   An optional user account for whom the list of repositories shall be rendered.
+    * @param repositoryOwnerName
+    *   The name of the user who owns the repository.
+    * @param repositoryName
+    *   The actual name of the repository.
+    * @return
+    *   An option to the file name that can be streamed to the user.
+    */
+  private def doDownloadDistribution(
+      csrf: Option[CsrfToken]
+  )(
+      user: Option[Account]
+  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Option[fs2.io.file.Path]] =
+    for {
+      owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+      loadedRepo <- owner match {
+        case None        => Sync[F].pure(None)
+        case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
+      }
+      // TODO Replace with whatever we implement as proper permission model. ;-)
+      repo = user match {
+        case None => loadedRepo.filter(r => r.isPrivate === false)
+        case Some(user) =>
+          loadedRepo.filter(r => r.isPrivate === false || r.owner === user.toVcsRepositoryOwner)
+      }
+      repositoryOwnerDirectory <- Sync[F].delay(
+        os.Path(
+          Paths.get(
+            darcsConfig.repositoriesDirectory.toPath.toString,
+            repositoryOwnerName.toString
+          )
+        )
+      )
+      targetDirectory <- Sync[F].delay(
+        os.Path(
+          Paths.get(
+            configuration.downloadDirectory.toPath.toString,
+            repositoryOwnerName.toString
+          )
+        )
+      )
+      createDist <- repo.traverse(repo =>
+        darcs.dist(repositoryOwnerDirectory.toNIO)(repositoryName.toString)(Chain.empty)
+      )
+      _ <- Sync[F].delay(repo.map(_ => Files.createDirectories(targetDirectory.toNIO)))
+      _ <- Sync[F].delay(repo.map(_ => os.remove(targetDirectory / s"${repositoryName.toString}.tar.gz")))
+      moveDist <- repo.traverse(_ =>
+        Sync[F].delay(
+          os.move.into(
+            repositoryOwnerDirectory / repositoryName.toString / s"${repositoryName.toString}.tar.gz",
+            targetDirectory
+          )
+        )
+      )
+      requestedFilePath <- repo.traverse(_ =>
+        Sync[F].delay(
+          fs2.io.file.Path.fromNioPath((targetDirectory / s"${repositoryName.toString}.tar.gz").toNIO)
+        )
+      )
+    } yield requestedFilePath
+
   /** Logic for rendering a list of all repositories visible to the given user account.
     *
     * @param csrf
@@ -538,6 +602,34 @@
       } yield resp
   }
 
+  private val downloadDistribution: AuthedRoutes[Account, F] = AuthedRoutes.of {
+    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "download" as user =>
+      for {
+        csrf <- Sync[F].delay(ar.req.getCsrfToken)
+        file <- doDownloadDistribution(csrf)(user.some)(repositoryOwnerName, repositoryName)
+        resp <- file match {
+          case None           => NotFound()
+          case Some(filePath) => StaticFile.fromPath(filePath, ar.req.some).getOrElseF(NotFound())
+        }
+      } yield resp
+  }
+
+  private val downloadDistributionForGuests: HttpRoutes[F] = HttpRoutes.of {
+    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "download" =>
+      for {
+        csrf <- Sync[F].delay(req.getCsrfToken)
+        file <- doDownloadDistribution(csrf)(None)(repositoryOwnerName, repositoryName)
+        resp <- file match {
+          case None           => NotFound()
+          case Some(filePath) => StaticFile.fromPath(filePath, req.some).getOrElseF(NotFound())
+        }
+      } yield resp
+  }
+
   private val forkRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
           repositoryName
@@ -798,10 +890,10 @@
   }
 
   val protectedRoutes =
-    forkRepository <+> showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
+    downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
 
   val routes =
-    cloneRepository <+> showAllRepositoriesForGuests <+> showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> showRepositoryFilesForGuests
+    cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+> showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> showRepositoryFilesForGuests
 
 }
 
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	2025-02-02 10:07:39.678646733 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	2025-02-02 10:07:39.682646743 +0000
@@ -55,6 +55,8 @@
                 </fieldset>
               </form>
             </dd>
+            <dt>@Messages("repository.overview.download.title")</dt>
+            <dd><a class="pure-button button-success" href="@{actionBaseUri.addSegment("download")}">@Messages("repository.overview.download.link")</a></dd>
             @for(account <- user) {
               @if(vcsRepository.owner === account.toVcsRepositoryOwner) {
               <!-- Cloning/Forking a repo to ourself is disabled. -->