{"action":"created","comment":{"url":"https://api.github.com/repos/slamdata/slamengine/pulls/comments/39061990","id":39061990,"diff_hunk":"@@ -343,73 +357,78 @@ final case class FileSystemApi(backend: Backend, contentPath: String, config: Co\n   def respond(v: EnvTask[String])(implicit E: EncodeJson[EnvironmentError]): Task[Response] =\n     v.fold(err => BadRequest(E.encode(err)), Ok(_)).join\n \n-  def mountService(config: Config, tester: BackendConfig => EnvTask[Unit], reloader: Config => Task[Unit]) = {\n-    def addPath(path: Path, req: Request): EnvTask[Boolean] = for {\n-      body <- liftT(EntityDecoder.decodeString(req))\n-      conf <- liftE(Parse.decodeEither[BackendConfig](body).leftMap(err => InvalidConfig(\"input error: \" + err)))\n-      _    <- liftE(conf.validate(path))\n-      _    <- tester(conf)\n-      _    <- liftT(reloader(config.copy(mountings = config.mountings + (path -> conf))))\n-    } yield config.mountings.keySet contains path\n+  def mountService(ref: TaskRef[Config]) = {\n+    def addPath(cfg: Config, path: Path, req: Request): EnvTask[Boolean] = for {\n+      body  <- liftT(EntityDecoder.decodeString(req))\n+      bConf <- liftE(Parse.decodeEither[BackendConfig](body).leftMap(err => InvalidConfig(\"input error: \" + err)))\n+      _     <- liftE(bConf.validate(path))\n+      _     <- tester(bConf)\n+      cfg   <- liftT(ref.read)\n+      _     <- liftT(writeConfig(ref, cfg.copy(mountings = cfg.mountings + (path -> bConf))))\n+    } yield cfg.mountings.keySet contains path\n+\n+    /**\n+     * Returns an error response if the given path overlaps any existing ones,\n+     * otherwise returns None.\n+     *\n+     * FIXME: This should really be checked in the backend, not here\n+     */\n+    def ensureNoOverlaps(cfg: Config, path: Path): Option[Task[Response]] =\n+      cfg.mountings.keys.toList.traverseU(k =>\n+        k.rebase(path)\n+          .as(Conflict(errorBody(s\"Can't add a mount point above the existing mount point at $k\", None)))\n+          .swap *>\n+        path.rebase(k)\n+          .as(Conflict(errorBody(s\"Can't add a mount point below the existing mount point at $k\", None)))\n+          .swap\n+      ).swap.toOption\n+\n \n     HttpService {\n       case GET -> AsPath(path) =>\n-        config.mountings.find { case (k, _) => k == path }.fold(\n-          NotFound(\"There is no mount point at \" + path))(\n-          v => Ok(BackendConfig.BackendConfig.encode(v._2)))\n+        ref.read.flatMap(_.mountings.find { case (k, _) => k == path }.cata(\n+          v => Ok(BackendConfig.BackendConfig.encode(v._2)),\n+          NotFound(s\"There is no mount point at $path\")))\n+\n       case req @ MOVE -> AsPath(path) =>\n-        (config.mountings.get(path), req.headers.get(Destination).map(_.value)) match {\n-          case (Some(mounting), Some(newPath)) =>\n-            mounting.validate(Path(newPath)).fold(\n-              err => BadRequest(errorBody(err.message, None)),\n-              κ(for {\n-                _    <- reloader(config.copy(mountings = config.mountings - path + (Path(newPath) -> mounting)))\n-                resp <- Ok(\"moved \" + path + \" to \" + newPath)\n-              } yield resp))\n-          case (None, _) => NotFound(errorBody(\"There is no mount point at \" + path, None))\n-          case (_, None) => DestinationHeaderMustExist\n-        }\n+        ref.read.flatMap(cfg =>\n+          (cfg.mountings.get(path), req.headers.get(Destination).map(_.value)) match {\n+            case (Some(mounting), Some(newPath)) =>\n+              mounting.validate(Path(newPath)).fold(\n+                err => BadRequest(errorBody(err.message, None)),\n+                κ(for {\n+                  newMnt <- (Path(newPath), mounting).point[Task]\n+                  _      <- writeConfig(ref, cfg.copy(mountings = cfg.mountings - path + newMnt))\n+                  resp   <- Ok(s\"moved $path to $newPath\")\n+                } yield resp))\n+\n+            case (None, _) =>\n+              NotFound(errorBody(s\"There is no mount point at $path\", None))\n+\n+            case (_, None) =>\n+              DestinationHeaderMustExist\n+          })\n+\n       case req @ POST -> AsPath(path) =>\n-        def addMount(newPath: Path) =\n-          respond(for {\n-            _ <- addPath(newPath, req)\n-          } yield \"added \" + newPath)\n-        req.headers.get(FileName).fold(\n-          FileNameHeaderMustExist) { nh =>\n+        req.headers.get(FileName) map { nh =>\n           val newPath = path ++ Path(nh.value)\n-          config.mountings.toList.map { case (k, _) =>\n-            // FIXME: This should really be checked in the backend, not here\n-            k.rebase(newPath).fold(\n-              κ(newPath.rebase(k).fold(\n-                κ(\\/-(())),\n-                κ(-\\/(Conflict(errorBody(\"Can't add a mount point below the existing mount point at  \" + k, None)))))),\n-              κ(-\\/(Conflict(errorBody(\"Can't add a mount point above the existing mount point at \" + k, None)))))\n-          }.sequenceU.fold(ɩ, κ(addMount(newPath)))\n-        }\n+          ref.read.flatMap(cfg =>\n+            ensureNoOverlaps(cfg, newPath) getOrElse\n+            respond(addPath(cfg, newPath, req) as s\"added $newPath\"))\n+        } getOrElse FileNameHeaderMustExist\n+\n       case req @ PUT -> AsPath(path) =>\n-        config.mountings.toList.map { case (k, _) =>\n-          // FIXME: This should really be checked in the backend, not here\n-          k.rebase(path).fold(\n-            κ(path.rebase(k).fold(\n-              κ(\\/-(())),\n-              κ(-\\/(Conflict(errorBody(\"Can't add a mount point below the existing mount point at  \" + k, None)))))),\n-            κ(if (k == path)\n-              \\/-(())\n-            else\n-              -\\/(Conflict(errorBody(\"Can't add a mount point above the existing mount point at \" + k, None)))))\n-        }.sequenceU.fold(\n-          ɩ,\n-          κ(respond(for {\n-            upd <- addPath(path, req)\n-          } yield (if (upd) \"updated\" else \"added\") + \" \" + path)))\n+        ref.read.flatMap(cfg =>\n+          (if (cfg.mountings contains path) None else ensureNoOverlaps(cfg, path)) getOrElse\n+          respond(addPath(cfg, path, req) map (upd => s\"${upd ? \"updated\" | \"added\"} $path\")))","path":"web/src/main/scala/slamdata/engine/api/fs.scala","position":182,"original_position":182,"commit_id":"43773e7aa5076941b7e8f12e39e9581fe4e8a37d","original_commit_id":"43773e7aa5076941b7e8f12e39e9581fe4e8a37d","user":{"login":"mossprescott","id":6147371,"avatar_url":"https://avatars.githubusercontent.com/u/6147371?v=3","gravatar_id":"","url":"https://api.github.com/users/mossprescott","html_url":"https://github.com/mossprescott","followers_url":"https://api.github.com/users/mossprescott/followers","following_url":"https://api.github.com/users/mossprescott/following{/other_user}","gists_url":"https://api.github.com/users/mossprescott/gists{/gist_id}","starred_url":"https://api.github.com/users/mossprescott/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mossprescott/subscriptions","organizations_url":"https://api.github.com/users/mossprescott/orgs","repos_url":"https://api.github.com/users/mossprescott/repos","events_url":"https://api.github.com/users/mossprescott/events{/privacy}","received_events_url":"https://api.github.com/users/mossprescott/received_events","type":"User","site_admin":false},"body":"Although we have no explicit policy, we've been avoiding string interpolation almost entirely, and certainly not putting logic in it. I know @jdegoes is not a fan; good time to make a decision and maybe add something to the wiki?","created_at":"2015-09-09T16:02:56Z","updated_at":"2015-09-09T16:02:56Z","html_url":"https://github.com/slamdata/slamengine/pull/910#discussion_r39061990","pull_request_url":"https://api.github.com/repos/slamdata/slamengine/pulls/910","_links":{"self":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/comments/39061990"},"html":{"href":"https://github.com/slamdata/slamengine/pull/910#discussion_r39061990"},"pull_request":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/910"}}},"pull_request":{"url":"https://api.github.com/repos/slamdata/slamengine/pulls/910","id":44372501,"html_url":"https://github.com/slamdata/slamengine/pull/910","diff_url":"https://github.com/slamdata/slamengine/pull/910.diff","patch_url":"https://github.com/slamdata/slamengine/pull/910.patch","issue_url":"https://api.github.com/repos/slamdata/slamengine/issues/910","number":910,"state":"open","locked":false,"title":"[SD-876] Only restart api server when port number changed","user":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"body":"Changes the behavior of `FileSystemApi` to only restart the api server when the port number is changed. For mounting changes, a local cache of the configuration is updated (in a `TaskRef`) after writing the updated config to disk.\r\n\r\nThe server main is also refactored in terms of `scalaz.stream.Process` which makes running + restarting uniform and allows us to test restarting the server deterministically.","created_at":"2015-09-07T17:26:55Z","updated_at":"2015-09-09T16:02:56Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2a0e742edc72ef88f9bb72b68edbfad9b63b18a3","assignee":{"login":"mossprescott","id":6147371,"avatar_url":"https://avatars.githubusercontent.com/u/6147371?v=3","gravatar_id":"","url":"https://api.github.com/users/mossprescott","html_url":"https://github.com/mossprescott","followers_url":"https://api.github.com/users/mossprescott/followers","following_url":"https://api.github.com/users/mossprescott/following{/other_user}","gists_url":"https://api.github.com/users/mossprescott/gists{/gist_id}","starred_url":"https://api.github.com/users/mossprescott/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mossprescott/subscriptions","organizations_url":"https://api.github.com/users/mossprescott/orgs","repos_url":"https://api.github.com/users/mossprescott/repos","events_url":"https://api.github.com/users/mossprescott/events{/privacy}","received_events_url":"https://api.github.com/users/mossprescott/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/slamdata/slamengine/pulls/910/commits","review_comments_url":"https://api.github.com/repos/slamdata/slamengine/pulls/910/comments","review_comment_url":"https://api.github.com/repos/slamdata/slamengine/pulls/comments{/number}","comments_url":"https://api.github.com/repos/slamdata/slamengine/issues/910/comments","statuses_url":"https://api.github.com/repos/slamdata/slamengine/statuses/43773e7aa5076941b7e8f12e39e9581fe4e8a37d","head":{"label":"wemrysi:ready/SD-876","ref":"ready/SD-876","sha":"43773e7aa5076941b7e8f12e39e9581fe4e8a37d","user":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"repo":{"id":40374964,"name":"slamengine","full_name":"wemrysi/slamengine","owner":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/wemrysi/slamengine","description":"The engine for SlamData","fork":true,"url":"https://api.github.com/repos/wemrysi/slamengine","forks_url":"https://api.github.com/repos/wemrysi/slamengine/forks","keys_url":"https://api.github.com/repos/wemrysi/slamengine/keys{/key_id}","collaborators_url":"https://api.github.com/repos/wemrysi/slamengine/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/wemrysi/slamengine/teams","hooks_url":"https://api.github.com/repos/wemrysi/slamengine/hooks","issue_events_url":"https://api.github.com/repos/wemrysi/slamengine/issues/events{/number}","events_url":"https://api.github.com/repos/wemrysi/slamengine/events","assignees_url":"https://api.github.com/repos/wemrysi/slamengine/assignees{/user}","branches_url":"https://api.github.com/repos/wemrysi/slamengine/branches{/branch}","tags_url":"https://api.github.com/repos/wemrysi/slamengine/tags","blobs_url":"https://api.github.com/repos/wemrysi/slamengine/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/wemrysi/slamengine/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/wemrysi/slamengine/git/refs{/sha}","trees_url":"https://api.github.com/repos/wemrysi/slamengine/git/trees{/sha}","statuses_url":"https://api.github.com/repos/wemrysi/slamengine/statuses/{sha}","languages_url":"https://api.github.com/repos/wemrysi/slamengine/languages","stargazers_url":"https://api.github.com/repos/wemrysi/slamengine/stargazers","contributors_url":"https://api.github.com/repos/wemrysi/slamengine/contributors","subscribers_url":"https://api.github.com/repos/wemrysi/slamengine/subscribers","subscription_url":"https://api.github.com/repos/wemrysi/slamengine/subscription","commits_url":"https://api.github.com/repos/wemrysi/slamengine/commits{/sha}","git_commits_url":"https://api.github.com/repos/wemrysi/slamengine/git/commits{/sha}","comments_url":"https://api.github.com/repos/wemrysi/slamengine/comments{/number}","issue_comment_url":"https://api.github.com/repos/wemrysi/slamengine/issues/comments{/number}","contents_url":"https://api.github.com/repos/wemrysi/slamengine/contents/{+path}","compare_url":"https://api.github.com/repos/wemrysi/slamengine/compare/{base}...{head}","merges_url":"https://api.github.com/repos/wemrysi/slamengine/merges","archive_url":"https://api.github.com/repos/wemrysi/slamengine/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/wemrysi/slamengine/downloads","issues_url":"https://api.github.com/repos/wemrysi/slamengine/issues{/number}","pulls_url":"https://api.github.com/repos/wemrysi/slamengine/pulls{/number}","milestones_url":"https://api.github.com/repos/wemrysi/slamengine/milestones{/number}","notifications_url":"https://api.github.com/repos/wemrysi/slamengine/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/wemrysi/slamengine/labels{/name}","releases_url":"https://api.github.com/repos/wemrysi/slamengine/releases{/id}","created_at":"2015-08-07T18:29:08Z","updated_at":"2015-08-07T18:29:09Z","pushed_at":"2015-09-08T15:12:15Z","git_url":"git://github.com/wemrysi/slamengine.git","ssh_url":"git@github.com:wemrysi/slamengine.git","clone_url":"https://github.com/wemrysi/slamengine.git","svn_url":"https://github.com/wemrysi/slamengine","homepage":null,"size":6682,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"slamdata:master","ref":"master","sha":"b28b4a7afe493ee47f76169126e73ea523e76488","user":{"login":"slamdata","id":5385602,"avatar_url":"https://avatars.githubusercontent.com/u/5385602?v=3","gravatar_id":"","url":"https://api.github.com/users/slamdata","html_url":"https://github.com/slamdata","followers_url":"https://api.github.com/users/slamdata/followers","following_url":"https://api.github.com/users/slamdata/following{/other_user}","gists_url":"https://api.github.com/users/slamdata/gists{/gist_id}","starred_url":"https://api.github.com/users/slamdata/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slamdata/subscriptions","organizations_url":"https://api.github.com/users/slamdata/orgs","repos_url":"https://api.github.com/users/slamdata/repos","events_url":"https://api.github.com/users/slamdata/events{/privacy}","received_events_url":"https://api.github.com/users/slamdata/received_events","type":"Organization","site_admin":false},"repo":{"id":16515058,"name":"slamengine","full_name":"slamdata/slamengine","owner":{"login":"slamdata","id":5385602,"avatar_url":"https://avatars.githubusercontent.com/u/5385602?v=3","gravatar_id":"","url":"https://api.github.com/users/slamdata","html_url":"https://github.com/slamdata","followers_url":"https://api.github.com/users/slamdata/followers","following_url":"https://api.github.com/users/slamdata/following{/other_user}","gists_url":"https://api.github.com/users/slamdata/gists{/gist_id}","starred_url":"https://api.github.com/users/slamdata/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slamdata/subscriptions","organizations_url":"https://api.github.com/users/slamdata/orgs","repos_url":"https://api.github.com/users/slamdata/repos","events_url":"https://api.github.com/users/slamdata/events{/privacy}","received_events_url":"https://api.github.com/users/slamdata/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/slamdata/slamengine","description":"A NoSQL analytics engine that embraces post-relational analytics and pushes computation to the data.","fork":false,"url":"https://api.github.com/repos/slamdata/slamengine","forks_url":"https://api.github.com/repos/slamdata/slamengine/forks","keys_url":"https://api.github.com/repos/slamdata/slamengine/keys{/key_id}","collaborators_url":"https://api.github.com/repos/slamdata/slamengine/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/slamdata/slamengine/teams","hooks_url":"https://api.github.com/repos/slamdata/slamengine/hooks","issue_events_url":"https://api.github.com/repos/slamdata/slamengine/issues/events{/number}","events_url":"https://api.github.com/repos/slamdata/slamengine/events","assignees_url":"https://api.github.com/repos/slamdata/slamengine/assignees{/user}","branches_url":"https://api.github.com/repos/slamdata/slamengine/branches{/branch}","tags_url":"https://api.github.com/repos/slamdata/slamengine/tags","blobs_url":"https://api.github.com/repos/slamdata/slamengine/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/slamdata/slamengine/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/slamdata/slamengine/git/refs{/sha}","trees_url":"https://api.github.com/repos/slamdata/slamengine/git/trees{/sha}","statuses_url":"https://api.github.com/repos/slamdata/slamengine/statuses/{sha}","languages_url":"https://api.github.com/repos/slamdata/slamengine/languages","stargazers_url":"https://api.github.com/repos/slamdata/slamengine/stargazers","contributors_url":"https://api.github.com/repos/slamdata/slamengine/contributors","subscribers_url":"https://api.github.com/repos/slamdata/slamengine/subscribers","subscription_url":"https://api.github.com/repos/slamdata/slamengine/subscription","commits_url":"https://api.github.com/repos/slamdata/slamengine/commits{/sha}","git_commits_url":"https://api.github.com/repos/slamdata/slamengine/git/commits{/sha}","comments_url":"https://api.github.com/repos/slamdata/slamengine/comments{/number}","issue_comment_url":"https://api.github.com/repos/slamdata/slamengine/issues/comments{/number}","contents_url":"https://api.github.com/repos/slamdata/slamengine/contents/{+path}","compare_url":"https://api.github.com/repos/slamdata/slamengine/compare/{base}...{head}","merges_url":"https://api.github.com/repos/slamdata/slamengine/merges","archive_url":"https://api.github.com/repos/slamdata/slamengine/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/slamdata/slamengine/downloads","issues_url":"https://api.github.com/repos/slamdata/slamengine/issues{/number}","pulls_url":"https://api.github.com/repos/slamdata/slamengine/pulls{/number}","milestones_url":"https://api.github.com/repos/slamdata/slamengine/milestones{/number}","notifications_url":"https://api.github.com/repos/slamdata/slamengine/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/slamdata/slamengine/labels{/name}","releases_url":"https://api.github.com/repos/slamdata/slamengine/releases{/id}","created_at":"2014-02-04T15:01:27Z","updated_at":"2015-09-08T00:51:08Z","pushed_at":"2015-09-09T00:01:57Z","git_url":"git://github.com/slamdata/slamengine.git","ssh_url":"git@github.com:slamdata/slamengine.git","clone_url":"https://github.com/slamdata/slamengine.git","svn_url":"https://github.com/slamdata/slamengine","homepage":"","size":10249,"stargazers_count":141,"watchers_count":141,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":36,"mirror_url":null,"open_issues_count":161,"forks":36,"open_issues":161,"watchers":141,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/910"},"html":{"href":"https://github.com/slamdata/slamengine/pull/910"},"issue":{"href":"https://api.github.com/repos/slamdata/slamengine/issues/910"},"comments":{"href":"https://api.github.com/repos/slamdata/slamengine/issues/910/comments"},"review_comments":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/910/comments"},"review_comment":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/slamdata/slamengine/pulls/910/commits"},"statuses":{"href":"https://api.github.com/repos/slamdata/slamengine/statuses/43773e7aa5076941b7e8f12e39e9581fe4e8a37d"}}}}