From d0611f0c4fd4369bbb920aaa4b41e52ba8bebe0a Mon Sep 17 00:00:00 2001 From: memo Date: Sun, 14 Apr 2024 15:08:20 +0200 Subject: [PATCH 1/2] rename Scope to Profile --- CHANGELOG.md | 2 +- Makefile | 2 +- api.md | 12 ++--- src/main/scala/sc4pac/Data.scala | 50 +++++++++---------- src/main/scala/sc4pac/Find.scala | 2 +- src/main/scala/sc4pac/ResolutionContext.scala | 2 +- src/main/scala/sc4pac/Sc4pac.scala | 34 ++++++------- src/main/scala/sc4pac/api/api.scala | 28 +++++------ src/main/scala/sc4pac/api/message.scala | 4 +- src/main/scala/sc4pac/cli.scala | 20 ++++---- src/main/scala/sc4pac/extractor.scala | 6 +-- src/main/scala/sc4pac/package.scala | 9 +--- 12 files changed, 82 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a89b3c7..501ec43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ ### Changed - The API was upgraded to version 1.1. -- The API now sends `/error/scope-not-initialized` & `/error/init/not-allowed` with HTTP status code 409 instead of 405. +- The API now sends `/error/profile-not-initialized` & `/error/init/not-allowed` with HTTP status code 409 instead of 405. - The API endpoint `/packages.list` now includes a `category` for each package. ### Fixed diff --git a/Makefile b/Makefile index 2e0b472..b40ee88 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ host-web: # channel-testing-web # sbt -Dcoursier.credentials="$(realpath sc4pac-credentials.properties)" clean: - rm -rf plugins temp sc4pac-plugins.json sc4pac-plugins-lock.json scopes + rm -rf plugins temp sc4pac-plugins.json sc4pac-plugins-lock.json profiles clean-cache: clean rm -rf cache diff --git a/api.md b/api.md index ab934e4..dab30c0 100644 --- a/api.md +++ b/api.md @@ -29,7 +29,7 @@ GET /update (websocket) - All endpoints may return some generic errors: - 400 (incorrect input) - 404 (non-existing packages, assets, etc.) - - 409 `/error/scope-not-initialized` (when not initialized) + - 409 `/error/profile-not-initialized` (when not initialized) - 500 (unexpected unresolvable situations) - 502 (download failures) - Errors are of the form @@ -43,8 +43,8 @@ GET /update (websocket) ## init -Initialize the scope by configuring the location of plugins and cache. -Scopes are used to manage multiple plugins folders. +Initialize the profile by configuring the location of plugins and cache. +Profiles are used to manage multiple plugins folders. Synopsis: `POST /init {plugins: "", cache: ""}` @@ -55,7 +55,7 @@ Returns: `platformDefaults: {plugins: ["", …], cache: ["", …]}` for recommended platform-specific locations to use. - ?> When managing multiple scopes, use the same cache for all of them. + ?> When managing multiple profiles, use the same cache for all of them. - 200 `{"$type": "/result", "ok": true}` on success. @@ -74,11 +74,11 @@ Returns: "platformDefaults": { "plugins": [ "/home/memo/Documents/SimCity 4/Plugins", - "/home/memo/git/sc4/sc4pac/scopes/scope-1/plugins" + "/home/memo/git/sc4/sc4pac/profiles/profile-1/plugins" ], "cache": [ "/home/memo/.cache/sc4pac", - "/home/memo/git/sc4/sc4pac/scopes/scope-1/cache" + "/home/memo/git/sc4/sc4pac/profiles/profile-1/cache" ] } } diff --git a/src/main/scala/sc4pac/Data.scala b/src/main/scala/sc4pac/Data.scala index 5f6030b..c21648e 100644 --- a/src/main/scala/sc4pac/Data.scala +++ b/src/main/scala/sc4pac/Data.scala @@ -45,15 +45,15 @@ object JsonData extends SharedData { variant: Variant, channels: Seq[java.net.URI] ) derives ReadWriter { - val pluginsRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(pluginsRoot, scopeRoot.path)) - val cacheRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(cacheRoot, scopeRoot.path)) - val tempRootAbs: RIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => os.Path(tempRoot, scopeRoot.path)) + val pluginsRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(pluginsRoot, profileRoot.path)) + val cacheRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(cacheRoot, profileRoot.path)) + val tempRootAbs: RIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => os.Path(tempRoot, profileRoot.path)) } object Config { - /** Turns an absolute path into a relative one if it is a subpath of scopeRoot, otherwise returns an absolute path. */ - def subRelativize(path: os.Path, scopeRoot: ScopeRoot): NioPath = { + /** Turns an absolute path into a relative one if it is a subpath of profileRoot, otherwise returns an absolute path. */ + def subRelativize(path: os.Path, profileRoot: ProfileRoot): NioPath = { try { - val sub: os.SubPath = path.subRelativeTo(scopeRoot.path) + val sub: os.SubPath = path.subRelativeTo(profileRoot.path) sub.toNIO } catch { case _: IllegalArgumentException => path.toNIO @@ -63,24 +63,24 @@ object JsonData extends SharedData { case class Plugins(config: Config, explicit: Seq[BareModule]) derives ReadWriter object Plugins { - def path(scopeRoot: os.Path): os.Path = scopeRoot / "sc4pac-plugins.json" + def path(profileRoot: os.Path): os.Path = profileRoot / "sc4pac-plugins.json" - def pathURIO: URIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => Plugins.path(scopeRoot.path)) + def pathURIO: URIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => Plugins.path(profileRoot.path)) private val projDirs = dev.dirs.ProjectDirectories.from("", cli.BuildInfo.organization, cli.BuildInfo.name) // qualifier, organization, application - val defaultPluginsRoot: URIO[ScopeRoot, Seq[os.Path]] = ZIO.serviceWith[ScopeRoot](scopeRoot => Seq( + val defaultPluginsRoot: URIO[ProfileRoot, Seq[os.Path]] = ZIO.serviceWith[ProfileRoot](profileRoot => Seq( os.home / "Documents" / "SimCity 4" / "Plugins", - scopeRoot.path / "plugins" + profileRoot.path / "plugins" )) - val defaultCacheRoot: URIO[ScopeRoot, Seq[os.Path]] = ZIO.serviceWith[ScopeRoot](scopeRoot => Seq( + val defaultCacheRoot: URIO[ProfileRoot, Seq[os.Path]] = ZIO.serviceWith[ProfileRoot](profileRoot => Seq( os.Path(java.nio.file.Paths.get(projDirs.cacheDir)), - scopeRoot.path / "cache" + profileRoot.path / "cache" )) /** Prompt for pluginsRoot and cacheRoot. This has a `CliPrompter` constraint as we only want to prompt about this using the CLI. */ - val promptForPaths: RIO[ScopeRoot & CliPrompter, (os.Path, os.Path)] = { + val promptForPaths: RIO[ProfileRoot & CliPrompter, (os.Path, os.Path)] = { val task = for { defaultPlugins <- defaultPluginsRoot pluginsRoot <- Prompt.paths("Choose the location of your Plugins folder. (It is recommended to start with an empty folder.)", defaultPlugins) @@ -95,15 +95,15 @@ object JsonData extends SharedData { } /** Init and write. */ - def init(pluginsRoot: os.Path, cacheRoot: os.Path): RIO[ScopeRoot, Plugins] = { + def init(pluginsRoot: os.Path, cacheRoot: os.Path): RIO[ProfileRoot, Plugins] = { for { - scopeRoot <- ZIO.service[ScopeRoot] - tempRoot <- ZIO.succeed(scopeRoot.path / "temp") // customization not needed + profileRoot <- ZIO.service[ProfileRoot] + tempRoot <- ZIO.succeed(profileRoot.path / "temp") // customization not needed data = Plugins( config = Config( - pluginsRoot = Config.subRelativize(pluginsRoot, scopeRoot), - cacheRoot = Config.subRelativize(cacheRoot, scopeRoot), - tempRoot = Config.subRelativize(tempRoot, scopeRoot), + pluginsRoot = Config.subRelativize(pluginsRoot, profileRoot), + cacheRoot = Config.subRelativize(cacheRoot, profileRoot), + tempRoot = Config.subRelativize(tempRoot, profileRoot), variant = Map.empty, channels = Constants.defaultChannelUrls), explicit = Seq.empty) @@ -112,7 +112,7 @@ object JsonData extends SharedData { } yield data } - val read: ZIO[ScopeRoot, ErrStr, Plugins] = Plugins.pathURIO.flatMap { pluginsPath => + val read: ZIO[ProfileRoot, ErrStr, Plugins] = Plugins.pathURIO.flatMap { pluginsPath => val task: IO[ErrStr | java.io.IOException, Plugins] = ZIO.ifZIO(ZIO.attemptBlockingIO(os.exists(pluginsPath)))( onFalse = ZIO.fail(s"Configuration file does not exist: $pluginsPath"), @@ -122,7 +122,7 @@ object JsonData extends SharedData { } /** Read Plugins from file if it exists, else create it and write it to file. */ - val readOrInit: RIO[ScopeRoot & CliPrompter, Plugins] = Plugins.pathURIO.flatMap { pluginsPath => + val readOrInit: RIO[ProfileRoot & CliPrompter, Plugins] = Plugins.pathURIO.flatMap { pluginsPath => ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsPath)))( onTrue = JsonIo.read[Plugins](pluginsPath), onFalse = for { @@ -171,12 +171,12 @@ object JsonData extends SharedData { } } object PluginsLock { - def path(scopeRoot: os.Path): os.Path = scopeRoot / "sc4pac-plugins-lock.json" + def path(profileRoot: os.Path): os.Path = profileRoot / "sc4pac-plugins-lock.json" - def pathURIO: URIO[ScopeRoot, os.Path] = ZIO.service[ScopeRoot].map(scopeRoot => PluginsLock.path(scopeRoot.path)) + def pathURIO: URIO[ProfileRoot, os.Path] = ZIO.service[ProfileRoot].map(profileRoot => PluginsLock.path(profileRoot.path)) /** Read PluginsLock from file if it exists, else create it and write it to file. */ - val readOrInit: RIO[ScopeRoot, PluginsLock] = PluginsLock.pathURIO.flatMap { pluginsLockPath => + val readOrInit: RIO[ProfileRoot, PluginsLock] = PluginsLock.pathURIO.flatMap { pluginsLockPath => ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsLockPath)))( onTrue = JsonIo.read[PluginsLock](pluginsLockPath), onFalse = { @@ -186,7 +186,7 @@ object JsonData extends SharedData { ) } - val listInstalled: RIO[ScopeRoot, Seq[DepModule]] = PluginsLock.pathURIO.flatMap { pluginsLockPath => + val listInstalled: RIO[ProfileRoot, Seq[DepModule]] = PluginsLock.pathURIO.flatMap { pluginsLockPath => ZIO.ifZIO(ZIO.attemptBlocking(os.exists(pluginsLockPath)))( onTrue = JsonIo.read[PluginsLock](pluginsLockPath).map(_.installed.map(_.toDepModule)), onFalse = ZIO.succeed(Seq.empty) diff --git a/src/main/scala/sc4pac/Find.scala b/src/main/scala/sc4pac/Find.scala index e99f35f..8091502 100644 --- a/src/main/scala/sc4pac/Find.scala +++ b/src/main/scala/sc4pac/Find.scala @@ -49,7 +49,7 @@ object Find { context.logger.log(s"Could not find metadata of ${module}. Trying to update channel contents.") for { repos <- Sc4pac.initializeRepositories(repoUris, context.cache, Constants.channelContentsTtlShort) // 60 seconds - .provideLayer(zio.ZLayer.succeed(ScopeRoot(context.scopeRoot))) + .provideLayer(zio.ZLayer.succeed(ProfileRoot(context.profileRoot))) result <- tryAllRepos(repos, context) // 2nd try } yield result } diff --git a/src/main/scala/sc4pac/ResolutionContext.scala b/src/main/scala/sc4pac/ResolutionContext.scala index 252801b..deb9ba5 100644 --- a/src/main/scala/sc4pac/ResolutionContext.scala +++ b/src/main/scala/sc4pac/ResolutionContext.scala @@ -7,7 +7,7 @@ class ResolutionContext( val repositories: Seq[MetadataRepository], val cache: FileCache, val logger: Logger, - val scopeRoot: os.Path + val profileRoot: os.Path ) { object coursierApi { diff --git a/src/main/scala/sc4pac/Sc4pac.scala b/src/main/scala/sc4pac/Sc4pac.scala index b87ab0f..9680827 100644 --- a/src/main/scala/sc4pac/Sc4pac.scala +++ b/src/main/scala/sc4pac/Sc4pac.scala @@ -17,9 +17,9 @@ import sc4pac.Resolution.{Dep, DepModule, DepAsset} // TODO Use `Runtime#reportFatal` or `Runtime.setReportFatal` to log fatal errors like stack overflow -class Sc4pac(val repositories: Seq[MetadataRepository], val cache: FileCache, val tempRoot: os.Path, val logger: Logger, val scopeRoot: os.Path) extends UpdateService { // TODO defaults +class Sc4pac(val repositories: Seq[MetadataRepository], val cache: FileCache, val tempRoot: os.Path, val logger: Logger, val profileRoot: os.Path) extends UpdateService { // TODO defaults - given context: ResolutionContext = new ResolutionContext(repositories, cache, logger, scopeRoot) + given context: ResolutionContext = new ResolutionContext(repositories, cache, logger, profileRoot) import CoursierZio.* // implicit coursier-zio interop @@ -27,13 +27,13 @@ class Sc4pac(val repositories: Seq[MetadataRepository], val cache: FileCache, va private def modifyExplicitModules[R](modify: Seq[BareModule] => ZIO[R, Throwable, Seq[BareModule]]): ZIO[R, Throwable, Seq[BareModule]] = { for { - pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(scopeRoot)) // at this point, file should already exist + pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(profileRoot)) // at this point, file should already exist modsOrig = pluginsData.explicit modsNext <- modify(modsOrig) _ <- ZIO.unless(modsNext == modsOrig) { val pluginsDataNext = pluginsData.copy(explicit = modsNext) // we do not check whether file was modified as this entire operation is synchronous and fast, in most cases - JsonIo.write(JD.Plugins.path(scopeRoot), pluginsDataNext, None)(ZIO.succeed(())) + JsonIo.write(JD.Plugins.path(profileRoot), pluginsDataNext, None)(ZIO.succeed(())) } } yield modsNext } @@ -191,7 +191,7 @@ trait UpdateService { this: Sc4pac => fallbackFilename, tempPluginsRoot / pkgFolder, recipe, - Some(Extractor.JarExtraction.fromUrl(art.url, cache, jarsRoot = jarsRoot, scopeRoot = scopeRoot)), + Some(Extractor.JarExtraction.fromUrl(art.url, cache, jarsRoot = jarsRoot, profileRoot = profileRoot)), hints = depAsset.archiveType, stagingRoot) // TODO catch IOExceptions @@ -263,7 +263,7 @@ trait UpdateService { this: Sc4pac => } /** Update all installed packages from modules (the list of explicitly added packages). */ - def update(modules: Seq[BareModule], globalVariant0: Variant, pluginsRoot: os.Path): RIO[ScopeRoot & Prompter, Boolean] = { + def update(modules: Seq[BareModule], globalVariant0: Variant, pluginsRoot: os.Path): RIO[ProfileRoot & Prompter, Boolean] = { // - before starting to remove anything, we download and extract everything // to install into temp folders (staging) @@ -342,7 +342,7 @@ trait UpdateService { this: Sc4pac => // - remove old packages // - move new packages into plugins folder // - write json database and release lock - val task = JsonIo.write(JD.PluginsLock.path(scopeRoot), pluginsLockData.updateTo(plan, staged.files.toMap), Some(pluginsLockData)) { + val task = JsonIo.write(JD.PluginsLock.path(profileRoot), pluginsLockData.updateTo(plan, staged.files.toMap), Some(pluginsLockData)) { for { _ <- remove(plan.toRemove, pluginsLockData.installed, pluginsRoot) // .catchAll(???) // TODO catch exceptions @@ -403,8 +403,8 @@ trait UpdateService { this: Sc4pac => } def storeGlobalVariant(globalVariant: Variant): Task[Unit] = for { - pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(scopeRoot)) // json file should exist already - _ <- JsonIo.write(JD.Plugins.path(scopeRoot), pluginsData.copy(config = pluginsData.config.copy(variant = globalVariant)), None)(ZIO.succeed(())) + pluginsData <- JsonIo.read[JD.Plugins](JD.Plugins.path(profileRoot)) // json file should exist already + _ <- JsonIo.write(JD.Plugins.path(profileRoot), pluginsData.copy(config = pluginsData.config.copy(variant = globalVariant)), None)(ZIO.succeed(())) } yield () // TODO catch coursier.error.ResolutionError$CantDownloadModule (e.g. when json files have syntax issues) @@ -471,7 +471,7 @@ object Sc4pac { case class StageResult(tempPluginsRoot: os.Path, files: Seq[(DepModule, Seq[os.SubPath])], stagingRoot: os.Path) - private def fetchChannelData(repoUri: java.net.URI, cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): ZIO[ScopeRoot, ErrStr, MetadataRepository] = { + private def fetchChannelData(repoUri: java.net.URI, cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): ZIO[ProfileRoot, ErrStr, MetadataRepository] = { import CoursierZio.* // implicit coursier-zio interop val contentsUrl = MetadataRepository.channelContentsUrl(repoUri).toString val artifact = Artifact(contentsUrl).withChanging(true) // changing as the remote file is updated whenever any remote package is added or updated @@ -481,8 +481,8 @@ object Sc4pac { .file(artifact) // requires initialized logger .run.absolve .mapError { case e @ (_: coursier.cache.ArtifactError | scala.util.control.NonFatal(_)) => e.getMessage } - scopeRoot <- ZIO.service[ScopeRoot] - repo <- MetadataRepository.create(os.Path(channelContentsFile: java.io.File, scopeRoot.path), repoUri) + profileRoot <- ZIO.service[ProfileRoot] + repo <- MetadataRepository.create(os.Path(channelContentsFile: java.io.File, profileRoot.path), repoUri) } yield repo } @@ -494,8 +494,8 @@ object Sc4pac { } yield result } - private[sc4pac] def initializeRepositories(repoUris: Seq[java.net.URI], cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): RIO[ScopeRoot, Seq[MetadataRepository]] = { - val task: RIO[ScopeRoot, Seq[MetadataRepository]] = ZIO.collectPar(repoUris) { url => + private[sc4pac] def initializeRepositories(repoUris: Seq[java.net.URI], cache: FileCache, channelContentsTtl: scala.concurrent.duration.Duration): RIO[ProfileRoot, Seq[MetadataRepository]] = { + val task: RIO[ProfileRoot, Seq[MetadataRepository]] = ZIO.collectPar(repoUris) { url => fetchChannelData(url, cache, channelContentsTtl) .mapError((err: ErrStr) => { System.err.println(s"Failed to read channel data: $err"); None }) }.filterOrFail(_.nonEmpty)(error.NoChannelsAvailable("No channels available", repoUris.toString)) @@ -505,7 +505,7 @@ object Sc4pac { wrapService(cache.logger.using(_), task) // properly initializes logger (avoids Uninitialized TermDisplay) } - def init(config: JD.Config): RIO[ScopeRoot & Logger, Sc4pac] = { + def init(config: JD.Config): RIO[ProfileRoot & Logger, Sc4pac] = { import CoursierZio.* // implicit coursier-zio interop // val refreshLogger = coursier.cache.loggers.RefreshLogger.create(System.err) // TODO System.err seems to cause less collisions between refreshing progress and ordinary log messages val coursierPool = coursier.cache.internal.ThreadUtil.fixedThreadPool(size = 2) // limit parallel downloads to 2 (ST rejects too many connections) @@ -517,8 +517,8 @@ object Sc4pac { // .withCachePolicies(Seq(coursier.cache.CachePolicy.ForceDownload)) // TODO cache policy repos <- initializeRepositories(config.channels, cache, Constants.channelContentsTtl) // 30 minutes tempRoot <- config.tempRootAbs - scopeRoot <- ZIO.service[ScopeRoot] - } yield Sc4pac(repos, cache, tempRoot, logger, scopeRoot.path) + profileRoot <- ZIO.service[ProfileRoot] + } yield Sc4pac(repos, cache, tempRoot, logger, profileRoot.path) } def parseModules(modules: Seq[String]): Either[ErrStr, Seq[BareModule]] = { diff --git a/src/main/scala/sc4pac/api/api.scala b/src/main/scala/sc4pac/api/api.scala index fb0cb4a..099259c 100644 --- a/src/main/scala/sc4pac/api/api.scala +++ b/src/main/scala/sc4pac/api/api.scala @@ -16,9 +16,9 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { private def jsonResponse[A : UP.Writer](obj: A): Response = Response.json(UP.write(obj, indent = options.indent)) private def jsonFrame[A : UP.Writer](obj: A): WebSocketFrame = WebSocketFrame.Text(UP.write(obj, indent = options.indent)) - /** Sends a 409 ScopeNotInitialized if Plugins cannot be loaded. */ - private val readPluginsOr409: ZIO[ScopeRoot, Response, JD.Plugins] = - JD.Plugins.read.mapError((err: ErrStr) => jsonResponse(ErrorMessage.ScopeNotInitialized("Scope not initialized", err)).status(Status.Conflict)) + /** Sends a 409 ProfileNotInitialized if Plugins cannot be loaded. */ + private val readPluginsOr409: ZIO[ProfileRoot, Response, JD.Plugins] = + JD.Plugins.read.mapError((err: ErrStr) => jsonResponse(ErrorMessage.ProfileNotInitialized("Profile not initialized", err)).status(Status.Conflict)) private def expectedFailureMessage(err: cli.Commands.ExpectedFailure): ErrorMessage = err match { case abort: error.Sc4pacVersionNotFound => ErrorMessage.VersionNotFound(abort.title, abort.detail) @@ -48,7 +48,7 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { } /** Handles some errors and provides http logger (not used for websocket). */ - private def wrapHttpEndpoint(task: ZIO[ScopeRoot & Logger, Throwable | Response, Response]): ZIO[ScopeRoot, Throwable, Response] = { + private def wrapHttpEndpoint(task: ZIO[ProfileRoot & Logger, Throwable | Response, Response]): ZIO[ProfileRoot, Throwable, Response] = { task.provideSomeLayer(httpLogger) .catchAll { case response: Response => ZIO.succeed(response) @@ -78,17 +78,17 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { .mapError(failedLabels => jsonResponse(errMsg(failedLabels)).status(Status.BadRequest)) } - def routes: Routes[ScopeRoot, Nothing] = Routes( + def routes: Routes[ProfileRoot, Nothing] = Routes( // 200, 400, 409 Method.POST / "init" -> handler { (req: Request) => wrapHttpEndpoint { for { - scopeRoot <- ZIO.serviceWith[ScopeRoot](_.path) - _ <- ZIO.attemptBlockingIO(os.exists(JD.Plugins.path(scopeRoot)) || os.exists(JD.PluginsLock.path(scopeRoot))) + profileRoot <- ZIO.serviceWith[ProfileRoot](_.path) + _ <- ZIO.attemptBlockingIO(os.exists(JD.Plugins.path(profileRoot)) || os.exists(JD.PluginsLock.path(profileRoot))) .filterOrFail(_ == false)(jsonResponse( - ErrorMessage.InitNotAllowed("Scope already initialized.", - "Manually delete the corresponding .json files if you are sure you want to initialize a new scope.") + ErrorMessage.InitNotAllowed("Profile already initialized.", + "Manually delete the corresponding .json files if you are sure you want to initialize a new profile.") ).status(Status.Conflict)) defPlugins <- JD.Plugins.defaultPluginsRoot defCache <- JD.Plugins.defaultCacheRoot @@ -97,8 +97,8 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { "Pass the locations of the folders as JSON dictionary: {plugins: , cache: }.", platformDefaults = Map("plugins" -> defPlugins.map(_.toString), "cache" -> defCache.map(_.toString)) )) - pluginsRoot = os.Path(initArgs.plugins, scopeRoot) - cacheRoot = os.Path(initArgs.cache, scopeRoot) + pluginsRoot = os.Path(initArgs.plugins, profileRoot) + cacheRoot = os.Path(initArgs.cache, profileRoot) _ <- ZIO.attemptBlockingIO { os.makeDir.all(pluginsRoot) // TODO ask for confirmation? os.makeDir.all(cacheRoot) @@ -145,7 +145,7 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { failure = ZIO.succeed[Response](_), success = pluginsData => Handler.webSocket { wsChannel => - val updateTask: zio.RIO[ScopeRoot & WebSocketLogger, Message] = + val updateTask: zio.RIO[ProfileRoot & WebSocketLogger, Message] = for { pac <- Sc4pac.init(pluginsData.config) pluginsRoot <- pluginsData.config.pluginsRootAbs @@ -154,7 +154,7 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { .provideSomeLayer(zio.ZLayer.succeed(WebSocketPrompter(wsChannel, wsLogger))) } yield ResultMessage(ok = true) - val wsTask: zio.RIO[ScopeRoot, Unit] = + val wsTask: zio.RIO[ProfileRoot, Unit] = WebSocketLogger.run(send = msg => wsChannel.send(Read(jsonFrame(msg)))) { for { finalMsg <- updateTask.catchSome { case err: cli.Commands.ExpectedFailure => ZIO.succeed(expectedFailureMessage(err)) } @@ -163,7 +163,7 @@ class Api(options: sc4pac.cli.Commands.ServerOptions) { } // wsLogger is shut down here (TODO use resource for safer closing) wsTask.zipRight(wsChannel.shutdown).map(_ => System.err.println("Shutting down websocket.")) - }.toResponse: zio.URIO[ScopeRoot, Response] + }.toResponse: zio.URIO[ProfileRoot, Response] ) }, diff --git a/src/main/scala/sc4pac/api/message.scala b/src/main/scala/sc4pac/api/message.scala index 517b3d0..0f2e5e7 100644 --- a/src/main/scala/sc4pac/api/message.scala +++ b/src/main/scala/sc4pac/api/message.scala @@ -84,8 +84,8 @@ sealed trait ErrorMessage extends Message derives UP.ReadWriter { object ErrorMessage { @upickle.implicits.key("/error/server-error") case class ServerError(title: String, detail: String) extends ErrorMessage derives UP.ReadWriter - @upickle.implicits.key("/error/scope-not-initialized") - case class ScopeNotInitialized(title: String, detail: String) extends ErrorMessage derives UP.ReadWriter + @upickle.implicits.key("/error/profile-not-initialized") + case class ProfileNotInitialized(title: String, detail: String) extends ErrorMessage derives UP.ReadWriter @upickle.implicits.key("/error/version-not-found") case class VersionNotFound(title: String, detail: String) extends ErrorMessage derives UP.ReadWriter @upickle.implicits.key("/error/asset-not-found") diff --git a/src/main/scala/sc4pac/cli.scala b/src/main/scala/sc4pac/cli.scala index 5984a4e..f679f59 100644 --- a/src/main/scala/sc4pac/cli.scala +++ b/src/main/scala/sc4pac/cli.scala @@ -26,7 +26,7 @@ object Commands { val cliEnvironment = { val logger = CliLogger() - zio.ZEnvironment(ScopeRoot(os.pwd), logger, CliPrompter(logger, autoYes = false)) + zio.ZEnvironment(ProfileRoot(os.pwd), logger, CliPrompter(logger, autoYes = false)) } // TODO strip escape sequences if jansi failed with a link error @@ -258,7 +258,7 @@ object Commands { runMainExit(task.provideEnvironment(cliEnvironment), exit) } - def iterateInstalled(pluginsData: JD.Plugins): zio.RIO[ScopeRoot, Iterator[(DepModule, Boolean)]] = { + def iterateInstalled(pluginsData: JD.Plugins): zio.RIO[ProfileRoot, Iterator[(DepModule, Boolean)]] = { for (installed <- JD.PluginsLock.listInstalled) yield { val sorted = installed.sortBy(mod => (mod.group.value, mod.name.value)) val explicit: Set[BareModule] = pluginsData.explicit.toSet @@ -311,7 +311,7 @@ object Commands { } } - def removeAndWrite(data: JD.Plugins, selected: Seq[String]): zio.RIO[ScopeRoot, Unit] = { + def removeAndWrite(data: JD.Plugins, selected: Seq[String]): zio.RIO[ProfileRoot, Unit] = { val data2 = data.copy(config = data.config.copy(variant = data.config.variant -- selected)) for { path <- JD.Plugins.pathURIO @@ -449,7 +449,7 @@ object Commands { |Start a local server to use the HTTP API. | |Example: - | sc4pac server --indent 2 --scope-root scopes/scope-1/ + | sc4pac server --indent 2 --profile-root profiles/profile-1/ """.stripMargin.trim) final case class ServerOptions( @ValueDescription("number") @Group("Server") @Tag("Server") @@ -461,22 +461,22 @@ object Commands { @ValueDescription("path") @Group("Server") @Tag("Server") @HelpMessage(s"root directory containing sc4pac-plugins.json (default: current working directory), newly created if necessary; " + "can be used for managing multiple different plugins folders") - scopeRoot: String = "", + profileRoot: String = "", ) extends Sc4pacCommandOptions case object Server extends Command[ServerOptions] { def run(options: ServerOptions, args: RemainingArgs): Unit = { if (options.indent < -1) error(caseapp.core.Error.Other(s"Indentation must be -1 or larger.")) - val scopeRoot: os.Path = if (options.scopeRoot.isEmpty) os.pwd else os.Path(java.nio.file.Paths.get(options.scopeRoot), os.pwd) - if (!os.exists(scopeRoot)) { - println(s"Creating sc4pac scope directory: $scopeRoot") - os.makeDir.all(scopeRoot) + val profileRoot: os.Path = if (options.profileRoot.isEmpty) os.pwd else os.Path(java.nio.file.Paths.get(options.profileRoot), os.pwd) + if (!os.exists(profileRoot)) { + println(s"Creating sc4pac profile directory: $profileRoot") + os.makeDir.all(profileRoot) } val task: Task[Unit] = { val app = sc4pac.api.Api(options).routes.toHttpApp println(s"Starting sc4pac server on port ${options.port}...") - zio.http.Server.serve(app).provide(zio.http.Server.defaultWithPort(options.port), zio.ZLayer.succeed(ScopeRoot(scopeRoot))) + zio.http.Server.serve(app).provide(zio.http.Server.defaultWithPort(options.port), zio.ZLayer.succeed(ProfileRoot(profileRoot))) } runMainExit(task, exit) } diff --git a/src/main/scala/sc4pac/extractor.scala b/src/main/scala/sc4pac/extractor.scala index 4b1e63e..405e5ac 100644 --- a/src/main/scala/sc4pac/extractor.scala +++ b/src/main/scala/sc4pac/extractor.scala @@ -231,10 +231,10 @@ object Extractor { */ case class JarExtraction(jarsDir: os.Path) object JarExtraction { - def fromUrl[F[_]](archiveUrl: String, cache: FileCache, jarsRoot: os.Path, scopeRoot: os.Path): JarExtraction = { + def fromUrl[F[_]](archiveUrl: String, cache: FileCache, jarsRoot: os.Path, profileRoot: os.Path): JarExtraction = { // we use cache to find a consistent archiveSubPath based on the url - val archivePath = os.Path(cache.localFile(archiveUrl), scopeRoot) - val cachePath = os.Path(cache.location, scopeRoot) + val archivePath = os.Path(cache.localFile(archiveUrl), profileRoot) + val cachePath = os.Path(cache.location, profileRoot) val archiveSubPath = archivePath.subRelativeTo(cachePath) // we hash the the archiveSubPath to keep paths short val hash = java.security.MessageDigest.getInstance("SHA-1") diff --git a/src/main/scala/sc4pac/package.scala b/src/main/scala/sc4pac/package.scala index 683cf57..1b54935 100644 --- a/src/main/scala/sc4pac/package.scala +++ b/src/main/scala/sc4pac/package.scala @@ -65,12 +65,5 @@ package object sc4pac { zio.Runtime.default.unsafe.run(effect).getOrThrowFiberFailure() } - // opaque type ScopeRoot = os.Path - // object ScopeRoot { - // def apply(path: os.Path): ScopeRoot = path - // } - // extension (scopeRoot: ScopeRoot) { - // def path: os.Path = scopeRoot - // } - class ScopeRoot(val path: os.Path) + class ProfileRoot(val path: os.Path) } From e9ea157577cf966ec7aca4dcf49bb5d7c71d8b76 Mon Sep 17 00:00:00 2001 From: memo Date: Sun, 14 Apr 2024 15:54:18 +0200 Subject: [PATCH 2/2] add deprecation for --scope-root --- CHANGELOG.md | 2 ++ api.md | 2 +- src/main/scala/sc4pac/cli.scala | 11 ++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 501ec43..534d051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Changed - decreased caching period of channel table-of-contents file from 24 hours to 30 minutes to receive package updates sooner +- The API was upgraded to version 1.2. +- The `sc4pac server` option `--scope-root` was renamed to `--profile-root` and the corresponding error to `/error/profile-not-initialized`. ## [0.4.1] - 2024-03-25 diff --git a/api.md b/api.md index dab30c0..58986bf 100644 --- a/api.md +++ b/api.md @@ -1,4 +1,4 @@ -# API - version 1.1 +# API - version 1.2 The API allows other programs to control *sc4pac* in a client-server fashion. diff --git a/src/main/scala/sc4pac/cli.scala b/src/main/scala/sc4pac/cli.scala index f679f59..873f34c 100644 --- a/src/main/scala/sc4pac/cli.scala +++ b/src/main/scala/sc4pac/cli.scala @@ -462,13 +462,22 @@ object Commands { @HelpMessage(s"root directory containing sc4pac-plugins.json (default: current working directory), newly created if necessary; " + "can be used for managing multiple different plugins folders") profileRoot: String = "", + @ValueDescription("path") @Group("Server") @Tag("Server") + @HelpMessage("deprecated (use --profile-root instead)") + scopeRoot: String = "", ) extends Sc4pacCommandOptions case object Server extends Command[ServerOptions] { def run(options: ServerOptions, args: RemainingArgs): Unit = { if (options.indent < -1) error(caseapp.core.Error.Other(s"Indentation must be -1 or larger.")) - val profileRoot: os.Path = if (options.profileRoot.isEmpty) os.pwd else os.Path(java.nio.file.Paths.get(options.profileRoot), os.pwd) + val profileRoot: os.Path = { + val optProfileRoot = if (options.scopeRoot.nonEmpty) { + println("Option --scope-root is deprecated. Use --profile-root instead.") + options.scopeRoot + } else options.profileRoot + if (optProfileRoot.isEmpty) os.pwd else os.Path(java.nio.file.Paths.get(optProfileRoot), os.pwd) + } if (!os.exists(profileRoot)) { println(s"Creating sc4pac profile directory: $profileRoot") os.makeDir.all(profileRoot)