From ed36a2ff0398a48d646b638089e5ce3b93c3ead9 Mon Sep 17 00:00:00 2001 From: Kirill Yankov Date: Mon, 13 May 2024 16:20:56 +0200 Subject: [PATCH] # 37, author, email, draft history --- project/scalatra/databusFormParam.mustache | 6 +- project/scalatra/databusQueryParam.mustache | 6 +- .../scala/org/dbpedia/databus/ApiImpl.scala | 147 ++++++++++++------ .../scala/org/dbpedia/databus/GitClient.scala | 65 ++++++-- .../dbpedia/databus/DatabusScalatraTest.scala | 24 +++ .../org/dbpedia/databus/LocalGitTest.scala | 65 ++++++-- swagger.yaml | 76 +++++++++ 7 files changed, 307 insertions(+), 82 deletions(-) diff --git a/project/scalatra/databusFormParam.mustache b/project/scalatra/databusFormParam.mustache index 9cf7bda..9a25854 100644 --- a/project/scalatra/databusFormParam.mustache +++ b/project/scalatra/databusFormParam.mustache @@ -1,8 +1,8 @@ {{#isFormParam}} - val {{paramName}} = Try(fileMultiParams("{{paramName}}").head).toOption + val {{paramName}} = Try(fileMultiParams("{{baseName}}").head).toOption .map(a => new String(a.get())) - .orElse(multiParams("{{paramName}}").headOption) + .orElse(multiParams("{{baseName}}").headOption) {{#required}} - .getOrElse(halt(400, "Form parameter {{paramName}} not specified.")) + .getOrElse(halt(400, "Form parameter {{baseName}} not specified.")) {{/required}} {{/isFormParam}} \ No newline at end of file diff --git a/project/scalatra/databusQueryParam.mustache b/project/scalatra/databusQueryParam.mustache index 2fbc01e..8370f12 100644 --- a/project/scalatra/databusQueryParam.mustache +++ b/project/scalatra/databusQueryParam.mustache @@ -1,9 +1,9 @@ {{#isQueryParam}} {{#required}} - val {{paramName}} = params.getAs[{{dataType}}]("{{paramName}}") - .getOrElse(halt(400, "Query parameter {{paramName}} not specified.")) + val {{paramName}} = params.getAs[{{dataType}}]("{{baseName}}") + .getOrElse(halt(400, "Query parameter {{baseName}} not specified.")) {{/required}} {{^required}} - val {{paramName}} = params.getAs[{{dataType}}]("{{paramName}}") + val {{paramName}} = params.getAs[{{dataType}}]("{{baseName}}") {{/required}} {{/isQueryParam}} \ No newline at end of file diff --git a/src/main/scala/org/dbpedia/databus/ApiImpl.scala b/src/main/scala/org/dbpedia/databus/ApiImpl.scala index 84881b2..5b4d5dc 100644 --- a/src/main/scala/org/dbpedia/databus/ApiImpl.scala +++ b/src/main/scala/org/dbpedia/databus/ApiImpl.scala @@ -12,7 +12,7 @@ import org.apache.jena.sys.JenaSystem import org.dbpedia.databus.ApiImpl.Config import org.dbpedia.databus.RdfConversions.{contextUrl, generateGraphId, graphToBytes, jenaJsonLdContextWithFallbackForLocalhost, mapContentType, readModel} import org.dbpedia.databus.swagger.api.DatabusApi -import org.dbpedia.databus.swagger.model.{OperationFailure, OperationSuccess} +import org.dbpedia.databus.swagger.model.{HistoryEntry, OperationFailure, OperationSuccess} import org.eclipse.jgit.errors.{MissingObjectException, RepositoryNotFoundException} import sttp.model.Uri import virtuoso.jdbc4.VirtuosoException @@ -32,6 +32,7 @@ class ApiImpl(config: Config) extends DatabusApi { init() def init() = JenaSystem.init() + def stop() = JenaSystem.shutdown() @@ -45,54 +46,72 @@ class ApiImpl(config: Config) extends DatabusApi { .flatMap(m => Tractate.extract(m._1.getGraph, TractateV1.Version)) .map(_.stringForSigning) - override def deleteFile(username: String, path: String, prefix: Option[String])(request: HttpServletRequest): Try[OperationSuccess] = { + override def deleteFile(username: String, + path: String, + prefix: Option[String], + author_name: Option[String], + author_email: Option[String])(request: HttpServletRequest): Try[OperationSuccess] = { val gid = generateGraphId(prefix.getOrElse(getPrefix(request)), username, path) - sparqlClient.executeUpdates( - RdfConversions.dropGraphSparqlQuery(gid) - )(m => { - if (m.map(_._2).sum > 0) { - deleteFileFromGit(username, path)(request) - .map(hash => OperationSuccess(gid, hash)) - } else { - Failure(new GraphDoesNotExistException(gid)) - } - }) + validateEmail(author_email) + .flatMap(email => + sparqlClient.executeUpdates( + RdfConversions.dropGraphSparqlQuery(gid) + )(m => { + if (m.map(_._2).sum > 0) { + deleteFileFromGit(username, path, author_name, email)(request) + .map(hash => OperationSuccess(gid, hash)) + } else { + Failure(new GraphDoesNotExistException(gid)) + } + }) + ) } - override def getFile(username: String, path: String)(request: HttpServletRequest): Try[String] = - readFile(username, path)(request) + override def getFile(repo: String, path: String)(request: HttpServletRequest): Try[String] = + readFile(repo, path)(request) + - override def saveFile(username: String, + override def saveFile(repo: String, path: String, body: String, - prefix: Option[String]) + prefix: Option[String], + author_name: Option[String], + author_email: Option[String]) (request: HttpServletRequest): Try[OperationSuccess] = { + val pa = gitPath(path) - val graphId = generateGraphId(prefix.getOrElse(getPrefix(request)), username, pa) + val graphId = generateGraphId(prefix.getOrElse(getPrefix(request)), repo, pa) val ct = Option(request.getContentType) .map(_.toLowerCase) .getOrElse("") val lang = mapContentType(ct, defaultLang) val ctxU = contextUrl(body.getBytes, lang) val ctx = ctxU.map(cu => jenaJsonLdContextWithFallbackForLocalhost(cu, request.getRemoteHost).get) - readModel(body.getBytes, lang, ctx) - .flatMap(model => { - saveToVirtuoso(model._1, graphId)({ - graphToBytes(model._1.getGraph, defaultLang, ctxU) - .flatMap(a => saveFiles(username, Map( - pa -> a - )).map(hash => OperationSuccess(graphId, hash))) - }).transform(Success(_), e => - if (model._2.isEmpty) { - Failure(e) - } else { - val ee = new RuntimeException( - s"Error saving data, potentially caused by: ${model._2.map(_.message).fold("")((l, r) => l + '\n' + r)}", - e) - ee.setStackTrace(Array.empty) - Failure(ee) - }) - }) + validateEmail(author_email).flatMap(email => + readModel(body.getBytes, lang, ctx) + .flatMap(model => { + saveToVirtuoso(model._1, graphId)({ + graphToBytes(model._1.getGraph, defaultLang, ctxU) + .flatMap(a => saveFiles( + repo, + Map( + pa -> a + ), + author_name, + email) + .map(hash => OperationSuccess(graphId, hash))) + }).transform(Success(_), e => + if (model._2.isEmpty) { + Failure(e) + } else { + val ee = new RuntimeException( + s"Error saving data, potentially caused by: ${model._2.map(_.message).fold("")((l, r) => l + '\n' + r)}", + e) + ee.setStackTrace(Array.empty) + Failure(ee) + }) + }) + ) } override def shaclValidate(dataid: String, shacl: String)(request: HttpServletRequest): Try[String] = { @@ -116,6 +135,7 @@ class ApiImpl(config: Config) extends DatabusApi { override def deleteFileMapException400(e: Throwable)(request: HttpServletRequest): Option[OperationFailure] = e match { case _: GraphDoesNotExistException => Some(OperationFailure(e.getMessage)) + case _: BadEmailException => Some(OperationFailure(e.getMessage)) case _ => None } @@ -134,12 +154,23 @@ class ApiImpl(config: Config) extends DatabusApi { Some(OperationFailure(s"Wrong value for type. ${e.getCause.getMessage}. ${e.getMessage}")) case _: RuntimeException if e.getCause.isInstanceOf[VirtuosoException] && e.getCause.getMessage.contains("SQ074") => Some(OperationFailure(s"Wrong input data. ${e.getCause.getMessage}. ${e.getMessage}")) + case _: BadEmailException => Some(OperationFailure(e.getMessage)) case _ => None } - override def shaclValidateMapException400(e: Throwable)(request: HttpServletRequest): Option[String] = e match { - case _ => Some(e.getMessage) - } + override def shaclValidateMapException400(e: Throwable)(request: HttpServletRequest): Option[OperationFailure] = + e match { + case _ => Some(OperationFailure(e.getMessage)) + } + + def history(repo: String, limit: Option[Int])(request: HttpServletRequest): scala.util.Try[List[HistoryEntry]] = + client.getHistory(repo, limit.getOrElse(10)).map(_.map(c => HistoryEntry(c.hash, c.author, c.email)).toList) + + def historyMapException400(e: Throwable)(request: HttpServletRequest): Option[OperationFailure] = + e match { + case _ => Some(OperationFailure(e.getMessage)) + } + private def getPrefix(request: HttpServletRequest): String = { val url = new URL(request.getRequestURL.toString) @@ -194,24 +225,30 @@ class ApiImpl(config: Config) extends DatabusApi { )(_ => execInTransaction) } - private def saveFileToGit(username: String, path: String, data: Array[Byte]): Try[String] = - saveFiles(username, Map(path -> data)) - - private def saveFiles(username: String, fullFilenamesAndData: Map[String, Array[Byte]]): Try[String] = + private def saveFiles(username: String, + fullFilenamesAndData: Map[String, Array[Byte]], + author_name: Option[String], + author_email: Option[String]): Try[String] = (if (!client.projectExists(username)) { client.createProject(username) } else { Success(Unit) - }).flatMap(_ => client.commitSeveralFiles(username, fullFilenamesAndData)) + }).flatMap(_ => client.commitSeveralFiles(username, fullFilenamesAndData, author_name, author_email)) - private def deleteFileFromGit(username: String, path: String)(request: HttpServletRequest): Try[String] = { + private def deleteFileFromGit(username: String, + path: String, + author_name: Option[String], + author_email: Option[String])(request: HttpServletRequest): Try[String] = { val p = gitPath(path) - deleteFiles(username, Seq(p))(request) + deleteFiles(username, Seq(p), author_name, author_email)(request) } - private def deleteFiles(username: String, paths: Seq[String])(request: HttpServletRequest): Try[String] = - client.deleteSeveralFiles(username, paths) + private def deleteFiles(username: String, + paths: Seq[String], + author_name: Option[String], + author_email: Option[String])(request: HttpServletRequest): Try[String] = + client.deleteSeveralFiles(username, paths, author_name, author_email) private def initGitClient(config: Config): GitClient = { import config._ @@ -232,6 +269,22 @@ class ApiImpl(config: Config) extends DatabusApi { object ApiImpl { + private final val Email_Pattern = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])".r + + def validateEmail(email: Option[String]): Try[Option[String]] = + email match { + case None => Success(None) + case Some(e) => Try { + if (Email_Pattern.unapplySeq(e).isDefined) { + email + } else { + throw new BadEmailException(e); + } + } + } + + class BadEmailException(email: String) extends Exception(s"The $email is not correct email.") + class GraphDoesNotExistException(id: String) extends Exception(s"Graph $id does not exist") case class Config(storageSparqlEndpointUri: Uri, diff --git a/src/main/scala/org/dbpedia/databus/GitClient.scala b/src/main/scala/org/dbpedia/databus/GitClient.scala index a4fa975..d763cd2 100644 --- a/src/main/scala/org/dbpedia/databus/GitClient.scala +++ b/src/main/scala/org/dbpedia/databus/GitClient.scala @@ -1,10 +1,11 @@ package org.dbpedia.databus +import org.dbpedia.databus.GitClient.{CommitDetails, nameAndEmail} + import java.nio.file.{Files, Path, Paths} import java.util.Base64 import java.util.concurrent.ConcurrentHashMap - import org.dbpedia.databus.RemoteGitlabHttpClient.{CreateFile, DeleteFile, FileAction, UpdateFile} import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.{Constants, Repository} @@ -17,30 +18,46 @@ import org.json4s._ import org.json4s.JsonDSL._ import org.json4s.jackson.JsonMethods._ +import scala.collection.JavaConverters.iterableAsScalaIterableConverter import scala.util.{Failure, Success, Try} trait GitClient { - def projectExists(name: String): Boolean def createProject(name: String): Try[String] - def commitFileContent(projectName: String, name: String, data: Array[Byte]): Try[String] = - commitSeveralFiles(projectName, Map(name -> data)) + def commitFileContent(projectName: String, name: String, data: Array[Byte], author_name: Option[String], author_email: Option[String]): Try[String] = + commitSeveralFiles(projectName, Map(name -> data), author_name, author_email) - def commitFileDelete(projectName: String, name: String): Try[String] = - deleteSeveralFiles(projectName, Seq(name)) + def commitFileDelete(projectName: String, name: String, author_name: Option[String], author_email: Option[String]): Try[String] = + deleteSeveralFiles(projectName, Seq(name), author_name, author_email) def readFile(projectName: String, name: String): Try[Array[Byte]] - def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]]): Try[String] + def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]], author_name: Option[String], author_email: Option[String]): Try[String] + + def deleteSeveralFiles(projectName: String, names: Seq[String], author_name: Option[String], author_email: Option[String]): Try[String] - def deleteSeveralFiles(projectName: String, names: Seq[String]): Try[String] + def getHistory(projectName: String, limit: Int): Try[Seq[CommitDetails]] + +} + +object GitClient { + case class CommitDetails(hash: String, author: String, email: String) + + def nameAndEmail(name: Option[String], email: Option[String]): (String, String) = (name, email) match { + case (Some(n), Some(e)) => (n, e) + case (Some(n), None) => (n, "") + case (None, Some(e)) => (e, e) + case _ => ("databus", "databus@infai.org") + } } class LocalGitClient(rootPath: Path) extends GitClient { + // todo maybe remove wrap with sync for read operations + // here we do not cleanup, assuming that the number os repos is relatively low (less than 1 000 000) private val locks = new ConcurrentHashMap[String, Object]() @@ -79,7 +96,7 @@ class LocalGitClient(rootPath: Path) extends GitClient { } } - override def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]]): Try[String] = + override def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]], author_name: Option[String], author_email: Option[String]): Try[String] = wrapWithSync(projectName) { Try({ val git = Git.open(getRepoPathFromUsername(projectName).toFile) @@ -95,8 +112,9 @@ class LocalGitClient(rootPath: Path) extends GitClient { } add.call() + val (name, email) = nameAndEmail(author_name, author_email) git.commit() - .setCommitter("databus", "databus@infai.org") + .setCommitter(name, email) .setMessage(s"$projectName: committed ${filenameAndData.keys}") .call() .getName @@ -104,7 +122,7 @@ class LocalGitClient(rootPath: Path) extends GitClient { } - override def deleteSeveralFiles(projectName: String, names: Seq[String]): Try[String] = + override def deleteSeveralFiles(projectName: String, names: Seq[String], author_name: Option[String], author_email: Option[String]): Try[String] = wrapWithSync(projectName) { Try { val git = Git.open(getRepoPathFromUsername(projectName).toFile) @@ -116,9 +134,10 @@ class LocalGitClient(rootPath: Path) extends GitClient { }) rm.call() + val (name, email) = nameAndEmail(author_name, author_email) val commit = git.commit() val hash = commit - .setCommitter("databus", "databus@infai.org") + .setCommitter(name, email) .setMessage(s"$projectName: removed $names") .call() .getName @@ -126,6 +145,19 @@ class LocalGitClient(rootPath: Path) extends GitClient { } } + override def getHistory(projectName: String, limit: Int): Try[Seq[CommitDetails]] = + wrapWithSync(projectName) { + Try { + Git.open(getRepoPathFromUsername(projectName).toFile).log().setMaxCount(limit).call().asScala.toSeq + .map(c => + CommitDetails( + c.getName, + c.getAuthorIdent.getName, + c.getAuthorIdent.getEmailAddress) + ) + } + } + private def wrapWithSync[R](name: String)(func: => R): R = locks.computeIfAbsent(name, _ => new Object) .synchronized(func) @@ -203,16 +235,20 @@ class RemoteGitlabHttpClient(rootUser: String, rootPass: String, scheme: String, projectIdByName(projectName) .flatMap(readSingleFile(_, name)) - override def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]]): Try[String] = + // todo: author name and email just ignored, this needs fix if we would want to use this feature some day with gitlab + override def commitSeveralFiles(projectName: String, filenameAndData: Map[String, Array[Byte]], author_name: Option[String], author_email: Option[String]): Try[String] = { projectIdByName(projectName) .flatMap(id => commitSeveralActions(id, filenameAndData.map(p => CreateFile(p._1, p._2)).toSeq) .orElse(commitSeveralActions(id, filenameAndData.map(p => UpdateFile(p._1, p._2)).toSeq)) ) + }.flatMap(_ => Try(throw new RuntimeException("author and email not implemented"))) - override def deleteSeveralFiles(projectName: String, names: Seq[String]): Try[String] = + // todo: author name and email just ignored, this needs fix if we would want to use this feature some day with gitlab + override def deleteSeveralFiles(projectName: String, names: Seq[String], author_name: Option[String], author_email: Option[String]): Try[String] = projectIdByName(projectName) .flatMap(id => commitSeveralActions(id, names.map(p => DeleteFile(p)))) + .flatMap(_ => Try(throw new RuntimeException("author and email not implemented"))) private def projectIdByName(name: String): Try[String] = { val req = withAuth( @@ -310,6 +346,7 @@ class RemoteGitlabHttpClient(rootUser: String, rootPass: String, scheme: String, }) } + override def getHistory(projectName: String, limit: Int): Try[Seq[CommitDetails]] = ??? } object RemoteGitlabHttpClient { diff --git a/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala b/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala index d872e8d..19221b2 100644 --- a/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala +++ b/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala @@ -61,10 +61,34 @@ class DatabusScalatraTest extends ScalatraFlatSpec with BeforeAndAfter { val file = "group.jsonld" val bytes = Files.readAllBytes(Paths.get(getClass.getClassLoader.getResource(file).getFile)) + post("/databus/graph/save?repo=kuckuck&path=pa/fl.jsonld&author_name=BlaBla&author_email=wrong", bytes) { + status should equal(400) + } + post("/databus/graph/save?repo=kuckuck&path=pa/fl.jsonld", bytes) { status should equal(200) } + post("/databus/graph/save?repo=kuckuck&path=pa/fl.jsonld&author_name=BlaBla", bytes) { + status should equal(200) + } + + post("/databus/graph/save?repo=kuckuck&path=pa/fl.jsonld&author_name=BlaBla&author_email=bla@bla.com", bytes) { + status should equal(200) + } + + post("/databus/graph/save?repo=kuckuck&path=pa/fl.jsonld&&author_email=bla@bla.com", bytes) { + status should equal(200) + } + + get("/databus/graph/history?repo=kuckuck&limit=2") { + status should equal(200) + body should include("author_name") + body should include("bla@bla.com") + body should include("BlaBla") + println(body) + } + get("/databus/graph/read?repo=kuckuck&path=pa/fl.jsonld") { status should equal(200) val respCtx = RdfConversions.contextUrl(bodyBytes, Lang.JSONLD10) diff --git a/src/test/scala/org/dbpedia/databus/LocalGitTest.scala b/src/test/scala/org/dbpedia/databus/LocalGitTest.scala index f097b68..102f8ca 100644 --- a/src/test/scala/org/dbpedia/databus/LocalGitTest.scala +++ b/src/test/scala/org/dbpedia/databus/LocalGitTest.scala @@ -24,10 +24,6 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { }) "Client" should "create and check project" in { - - - val rp = Paths.get(root).resolve("test_root") - val cli = new LocalGitClient(rp) val re = cli.createProject("gut") @@ -37,14 +33,42 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { cli.projectExists("gut") should be(true) } + "Client" should "commit with different author names and emails" in { + val cli = new LocalGitClient(rp) + val pn = "test" + cli.createProject(pn) + Seq( + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), None, None), + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), None, Some("bla")), + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), Some("Bla1"), None), + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), Some("Bla2"), Some("")), + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), Some("Bla3"), Some("e@mail.mail")), + cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes), Some("Bla4"), Some("stri")), + ).foreach(_.isSuccess should be(true)) + + val ts = Seq( + ("Bla4", "stri"), + ("Bla3", "e@mail.mail"), + ("Bla2", ""), + ("Bla1", ""), + ("bla", "bla"), + ("databus", "databus@infai.org") + ) + + allCommits(pn).zip(ts).foreach(c => { + c._1.getAuthorIdent.getName should be(c._2._1) + c._1.getAuthorIdent.getEmailAddress should be(c._2._2) + }) + + } + "Client" should "commit twice" in { - val rp = Paths.get(root).resolve("test_root") val cli = new LocalGitClient(rp) val pn = "test" cli.createProject(pn) - val re2 = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes)) - val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes)) + val re2 = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes), Some("Bla"), None) + val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes), Some("Bla"), None) re2.isSuccess should be(true) re.isSuccess should be(true) @@ -78,12 +102,11 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { } "Client" should "delete twice" in { - val rp = Paths.get(root).resolve("test_root") val cli = new LocalGitClient(rp) val pn = "test" cli.createProject(pn) - val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes)) + val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes), Some("Bla"), None) re.isSuccess should be(true) @@ -94,21 +117,19 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { "lol.txt" ) - val re2 = cli.deleteSeveralFiles("test", files.toSeq) + val re2 = cli.deleteSeveralFiles("test", files.toSeq, Some("Bla"), None) re2.isSuccess should be(true) - val re3 = cli.deleteSeveralFiles("test", files.toSeq) + val re3 = cli.deleteSeveralFiles("test", files.toSeq, Some("Bla"), None) re3.isSuccess should be(true) } "Client" should "commit files" in { - - val rp = Paths.get(root).resolve("test_root") val cli = new LocalGitClient(rp) val pn = "test" cli.createProject(pn) - val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes)) + val re = cli.commitSeveralFiles(pn, Map("haha.txt" -> "lol".getBytes, "/loldata/lol.txt" -> "haha".getBytes), Some("Bla"), None) re.isSuccess should be(true) @@ -132,7 +153,7 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { treeWalk.next() should be(false) - val red = cli.deleteSeveralFiles(pn, files.toSeq) + val red = cli.deleteSeveralFiles(pn, files.toSeq, Some("Bla"), None) red.isSuccess should be(true) val entrs2 = diff(pn) @@ -144,6 +165,20 @@ class LocalGitTest extends FlatSpec with Matchers with BeforeAndAfter { }) } + + def lastCommit(repoName: String) = { + val git = Git.open(rp.resolve(repoName).toFile) + val rw = new RevWalk(git.getRepository) + val head = git.getRepository.resolve(Constants.HEAD) + rw.parseCommit(head) + } + + + def allCommits(repoName: String) = { + val git = Git.open(rp.resolve(repoName).toFile) + git.log().call().asScala + } + def treeWalkForLastCommit(repoName: String) = { val git = Git.open(rp.resolve(repoName).toFile) val rw = new RevWalk(git.getRepository) diff --git a/swagger.yaml b/swagger.yaml index f17e684..0f8ab41 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -36,6 +36,18 @@ paths: type: string required: false example: "http://foreighhost/api" + - in: query + name: author_name + description: "Name of a person who saves the data." + type: string + required: false + example: "John Snow" + - in: query + name: author_email + description: "An email of a person who saves the data." + type: string + required: false + example: "bla@bla.bla" - in: body name: body description: "Graph data in Turtle or JSONLD." @@ -86,6 +98,18 @@ paths: type: string required: false example: "http://foreighhost/api" + - in: query + name: author_name + description: "Name of a person deleting file." + type: string + required: false + example: "John Snows" + - in: query + name: author_email + description: "Email of a person deleting file." + type: string + required: false + example: "John Snows" responses: "200": description: "Successful operation." @@ -126,6 +150,38 @@ paths: description: "Not found." schema: $ref: "#/definitions/OperationFailure" + /graph/history: + get: + summary: "Graph history" + description: "Get graph history for particular repo." + operationId: "history" + produces: + - "application/json" + parameters: + - in: query + name: repo + description: "Repository name in which the graph is stored." + type: string + required: true + example: "testuser" + - in: query + name: limit + description: "Number of latest history entries to query." + type: integer + required: false + default: 10 + example: 1 + responses: + "200": + description: "List of history entries details." + schema: + type: array + items: + $ref: "#/definitions/HistoryEntry" + "400": + description: "Returns a description of an error." + schema: + $ref: "#/definitions/OperationFailure" /shacl/validate: post: summary: "Validate a graph with SHACL." @@ -157,6 +213,8 @@ paths: format: binary "400": description: "Invalid input." + schema: + $ref: "#/definitions/OperationFailure" /dataid/tractate: post: summary: "DataID tractate." @@ -180,6 +238,23 @@ paths: schema: type: string definitions: + + HistoryEntry: + properties: + commit_hash: + description: "hash of a commit of the changes" + type: "string" + author_name: + description: "name of a person who did the changes" + type: "string" + author_email: + description: "email of a person who did the changes" + type: "string" + required: + - commit_hash + - author_name + - author_email + OperationSuccess: properties: graphid: @@ -191,6 +266,7 @@ definitions: required: - graphid - commit_hash + OperationFailure: properties: message: