~jan0sch/smederee
Showing details for patch 4c4f84e46f317725351692477c09b62a47f660b7.
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. -->