Skip to content
This repository was archived by the owner on Sep 27, 2021. It is now read-only.

Commit f4520c4

Browse files
authored
Merging from master fixes from library dependencies (#27)
* Upgraded kamon, disabled by default, instrumentation via agent (#9) * Added asynchronous digest computation (#8) * Added digest computation * Fixed docs * Added Accepted status code when Digest is empty * Incorporated feedback * Used java Cock * Reverted to compute digest on create (#10) * Removed unnecessary Content-Disposition header (#11) * Added handling client errors as unexpected server errors (#13) * Add 'nexus-fixer' (#12) * Add Rust project and target in build.sbt * Add call to 'nexus-fixer' binary from the service * Update nexus-fixer README (#14) * Bumped nexus dependencies (#15) * Add sbt-assembly to build a fat JAR (#16) * Add links detection to 'nexus-fixer' (#17) * Fix assembly deduplication errors caused by Kamon (#18) * Bumped sbt-nexus, nexus-commons and nexus-sourcing deps. (#19) * Bumped sbt-nexus, nexus-commons and nexus-sourcing deps. * Fixed formatting due to bump to scalfmt 2.0.0 * Fixed create empty file (#20) * Added mediaType information on creation/move response (#21) * Added mediaType information on creation/move response * Reverted app.conf changes and added dir to detectMediaType * Change dir mask to 750 (#22) * Added javac flags for java 8 (#23) * Bumped nexus-commons dependency (#24) * Bumped deps. and increased SSE parsing size (#25) * Bumped nexus-commons dependencies (#26) * Bumped iam version
1 parent 9a108ec commit f4520c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2266
-256
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ native
1111
# Maven / SBT / Ant target #
1212
target/
1313
.bloop
14-
project/.bloop
1514

1615
# IDE
1716
.settings
@@ -21,6 +20,9 @@ project/.bloop
2120
**/logs/
2221
**/nbactions.xml
2322

23+
# Rust
24+
**/*.rs.bk
25+
Cargo.lock
2426

2527
# Packages #
2628
############

.scalafmt.conf

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
style = defaultWithAlign
22
maxColumn = 120
3-
rewrite.rules = [SortImports]
3+
version = 2.0.0
4+
align.tokens = [
5+
{ code = "=>", owner = "Case" }
6+
{ code = "⇒", owner = "Case" }
7+
{ code = "extends", owner = "Defn.(Class|Trait|Object)" }
8+
{ code = "//", owner = ".*" }
9+
{ code = "{", owner = "Template" }
10+
{ code = "}", owner = "Template" }
11+
{ code = ":=", owner = "Term.ApplyInfix" }
12+
{ code = "++=", owner = "Term.ApplyInfix" }
13+
{ code = "+=", owner = "Term.ApplyInfix" }
14+
{ code = "%", owner = "Term.ApplyInfix" }
15+
{ code = "%%", owner = "Term.ApplyInfix" }
16+
{ code = "%%%", owner = "Term.ApplyInfix" }
17+
{ code = "->", owner = "Term.ApplyInfix" }
18+
{ code = "→", owner = "Term.ApplyInfix" }
19+
{ code = "<-", owner = "Enumerator.Generator" }
20+
{ code = "←", owner = "Enumerator.Generator" }
21+
{ code = "=", owner = "(Enumerator.Val|Defn.(Va(l|r)|Def|Type))" }
22+
]
23+
rewrite.rules = [SortImports]

Jenkinsfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ pipeline {
77
options {
88
timeout(time: 30, unit: 'MINUTES')
99
}
10+
environment {
11+
NEXUS_PATH_PREFIX = """${sh(returnStdout: true, script: 'oc get configmap storage -o jsonpath="{.data.path_prefix}"')}"""
12+
NEXUS_USER_ID = """${sh(returnStdout: true, script: 'oc get configmap storage -o jsonpath="{.data.user_id}"')}"""
13+
NEXUS_GROUP_ID = """${sh(returnStdout: true, script: 'oc get configmap storage -o jsonpath="{.data.group_id}"')}"""
14+
}
1015
stages {
1116
stage("Review") {
1217
when {
@@ -39,6 +44,7 @@ pipeline {
3944
}
4045
steps {
4146
checkout scm
47+
sh 'echo $NEXUS_PATH_PREFIX $NEXUS_USER_ID $NEXUS_GROUP_ID'
4248
sh 'sbt releaseEarly universal:packageZipTarball'
4349
stash name: "service", includes: "target/universal/storage-*.tgz"
4450
}

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44

55
# Nexus Storage Service
66

7-
A service that is intended to abstract I/O file system operations with an API to deal with uploads and downloads of files.
8-
7+
A service to abstract I/O operations on a remote file system, to support Nexus' file management API.
98

109
Please visit the [parent project](https://github.com/BlueBrain/nexus) for more information about Nexus.
1110

11+
## Build
12+
13+
In the project's top-level directory run:
14+
15+
```shell script
16+
./sbt assembly
17+
```
18+
19+
This outputs a self-contained JAR `nexus-storage.jar`.
1220

1321
## Getting involved
1422
There are several channels provided to address different issues:

build.sbt

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ scalafmt: {
2525
*/
2626

2727
// Dependency versions
28-
val akkaVersion = "2.5.23"
28+
val akkaVersion = "2.5.25"
2929
val akkaHttpVersion = "10.1.9"
3030
val apacheCompressVersion = "1.18"
31-
val alpakkaVersion = "1.0.2"
31+
val alpakkaVersion = "1.1.1"
3232
val catsVersion = "1.6.1"
33-
val catsEffectVersion = "1.3.1"
33+
val catsEffectVersion = "1.4.0"
3434
val circeVersion = "0.11.1"
35-
val commonsVersion = "0.17.0"
36-
val iamVersion = "1.1.1"
37-
val mockitoVersion = "1.5.11"
35+
val commonsVersion = "0.17.6"
36+
val iamVersion = "1.1.2"
37+
val mockitoVersion = "1.5.14"
3838
val monixVersion = "3.0.0-RC3"
3939
val pureconfigVersion = "0.11.1"
4040
val scalaTestVersion = "3.0.8"
@@ -60,13 +60,14 @@ lazy val scalaTest = "org.scalatest" %% "scalatest"
6060

6161
lazy val storage = project
6262
.in(file("."))
63-
.settings(testSettings, buildInfoSettings)
63+
.settings(assemblySettings, testSettings, buildInfoSettings)
6464
.enablePlugins(BuildInfoPlugin, ServicePackagingPlugin)
6565
.aggregate(client)
6666
.settings(
67-
name := "storage",
68-
moduleName := "storage",
69-
coverageFailOnMinimum := true,
67+
name := "storage",
68+
moduleName := "storage",
69+
coverageFailOnMinimum := true,
70+
javaSpecificationVersion := "1.8",
7071
libraryDependencies ++= Seq(
7172
apacheCompress,
7273
akkaHttp,
@@ -86,8 +87,12 @@ lazy val storage = project
8687
mockito % Test,
8788
scalaTest % Test
8889
),
90+
cleanFiles ++= Seq(
91+
baseDirectory.value / "permissions-fixer" / "target" / "**",
92+
baseDirectory.value / "nexus-storage.jar"
93+
),
8994
mappings in Universal := {
90-
val universalMappings = (mappings in Universal).value
95+
val universalMappings = (mappings in Universal).value :+ cargo.value
9196
universalMappings.foldLeft(Vector.empty[(File, String)]) {
9297
case (acc, (file, filename)) if filename.contains("kanela-agent") =>
9398
acc :+ (file, "lib/instrumentation-agent.jar")
@@ -99,11 +104,13 @@ lazy val storage = project
99104

100105
lazy val client = project
101106
.in(file("client"))
107+
.disablePlugins(AssemblyPlugin)
102108
.settings(
103109
testSettings,
104-
name := "storage-client",
105-
moduleName := "storage-client",
106-
coverageFailOnMinimum := true,
110+
name := "storage-client",
111+
moduleName := "storage-client",
112+
coverageFailOnMinimum := true,
113+
javaSpecificationVersion := "1.8",
107114
libraryDependencies ++= Seq(
108115
akkaHttp,
109116
akkaStream,
@@ -114,10 +121,24 @@ lazy val client = project
114121
akkaHttpTestKit % Test,
115122
commonsTest % Test,
116123
mockito % Test,
117-
scalaTest % Test,
124+
scalaTest % Test
118125
)
119126
)
120127

128+
lazy val assemblySettings = Seq(
129+
test in assembly := {},
130+
assemblyOutputPath in assembly := baseDirectory.value / "nexus-storage.jar",
131+
assemblyMergeStrategy in assembly := {
132+
case PathList("org", "apache", "commons", "logging", xs @ _*) => MergeStrategy.last
133+
case PathList("akka", "remote", "kamon", xs @ _*) => MergeStrategy.last
134+
case PathList("kamon", "instrumentation", "akka", "remote", xs @ _*) => MergeStrategy.last
135+
case "META-INF/versions/9/module-info.class" => MergeStrategy.discard
136+
case x =>
137+
val oldStrategy = (assemblyMergeStrategy in assembly).value
138+
oldStrategy(x)
139+
}
140+
)
141+
121142
lazy val testSettings = Seq(
122143
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-o", "-u", "target/test-reports"),
123144
Test / parallelExecution := false
@@ -128,6 +149,22 @@ lazy val buildInfoSettings = Seq(
128149
buildInfoPackage := "ch.epfl.bluebrain.nexus.storage.config"
129150
)
130151

152+
lazy val cargo = taskKey[(File, String)]("Run Cargo to build 'nexus-fixer'")
153+
154+
cargo := {
155+
import scala.sys.process._
156+
157+
val log = streams.value.log
158+
val cmd = Process(Seq("cargo", "build", "--release"), baseDirectory.value / "permissions-fixer")
159+
if ((cmd !) == 0) {
160+
log.success("Cargo build successful.")
161+
(baseDirectory.value / "permissions-fixer" / "target" / "release" / "nexus-fixer") -> "bin/nexus-fixer"
162+
} else {
163+
log.error("Cargo build failed.")
164+
throw new RuntimeException
165+
}
166+
}
167+
131168
inThisBuild(
132169
List(
133170
homepage := Some(url("https://github.com/BlueBrain/nexus-storage")),

client/src/main/scala/ch/epfl/bluebrain/nexus/storage/client/StorageClient.scala

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import ch.epfl.bluebrain.nexus.iam.client.types._
2323
import ch.epfl.bluebrain.nexus.storage.client.StorageClient._
2424
import ch.epfl.bluebrain.nexus.storage.client.StorageClientError._
2525
import ch.epfl.bluebrain.nexus.storage.client.config.StorageClientConfig
26+
import ch.epfl.bluebrain.nexus.storage.client.types.FileAttributes.Digest
2627
import ch.epfl.bluebrain.nexus.storage.client.types.{FileAttributes, LinkFile, ServiceDescription}
2728
import io.circe
2829
import io.circe.parser.parse
@@ -31,17 +32,17 @@ import journal.Logger
3132

3233
import scala.concurrent.{ExecutionContext, Future}
3334
import scala.reflect.ClassTag
35+
import scala.util.control.NonFatal
3436

3537
class StorageClient[F[_]] private[client] (
3638
config: StorageClientConfig,
3739
attributes: HttpClient[F, FileAttributes],
40+
digest: HttpClient[F, Digest],
3841
source: HttpClient[F, AkkaSource],
3942
serviceDesc: HttpClient[F, ServiceDescription],
4043
emptyBody: HttpClient[F, NotUsed]
4144
)(implicit F: Effect[F], ec: ExecutionContext) {
4245

43-
private val emptyChunk = "An HttpEntity.Chunk must have non-empty data"
44-
4546
def serviceDescription: F[ServiceDescription] =
4647
serviceDesc(Get(config.iri.toAkkaUri))
4748

@@ -66,15 +67,15 @@ class StorageClient[F[_]] private[client] (
6667
* @return The file attributes wrapped on the effect type F[] containing the metadata plus the file bytes, digest and location
6768
*/
6869
def createFile(name: String, relativePath: Uri.Path, source: AkkaSource)(
69-
implicit cred: Option[AuthToken]): F[FileAttributes] = {
70+
implicit cred: Option[AuthToken]
71+
): F[FileAttributes] = {
7072
val endpoint = config.files(name) + slashIfNone(relativePath).toIriPath
7173
val bodyPartEntity = HttpEntity.IndefiniteLength(ContentTypes.`application/octet-stream`, source)
7274
val filename = extractName(relativePath).getOrElse("myfile")
7375
val multipartForm = FormData(BodyPart("file", bodyPartEntity, Map("filename" -> filename))).toEntity()
7476
attributes(Put(endpoint.toAkkaUri, multipartForm).withCredentials).recoverWith {
75-
case ex: IllegalArgumentException if ex.getMessage != null && ex.getMessage.endsWith(emptyChunk) =>
76-
createFile(name, relativePath, Source.empty)
77-
case ex => F.raiseError(ex)
77+
case EmptyChunk => createFile(name, relativePath, Source.empty)
78+
case ex => F.raiseError(ex)
7879
}
7980
}
8081

@@ -96,6 +97,17 @@ class StorageClient[F[_]] private[client] (
9697
source(Get(endpoint.toAkkaUri).withCredentials)
9798
}
9899

100+
/**
101+
* Retrieves the file digest.
102+
*
103+
* @param name the storage bucket name
104+
* @param relativePath the relative path to the file location
105+
*/
106+
def getDigest(name: String, relativePath: Uri.Path)(implicit cred: Option[AuthToken]): F[Digest] = {
107+
val endpoint = config.digests(name) + slashIfNone(relativePath).toIriPath
108+
digest(Get(endpoint.toAkkaUri).withCredentials)
109+
}
110+
99111
/**
100112
* Moves a path from the provided ''sourceRelativePath'' to ''destRelativePath'' inside the nexus folder.
101113
*
@@ -105,7 +117,8 @@ class StorageClient[F[_]] private[client] (
105117
* @return The file attributes wrapped on the effect type F[] containing the metadata plus the file bytes, digest and location
106118
*/
107119
def moveFile(name: String, sourceRelativePath: Uri.Path, destRelativePath: Uri.Path)(
108-
implicit cred: Option[AuthToken]): F[FileAttributes] = {
120+
implicit cred: Option[AuthToken]
121+
): F[FileAttributes] = {
109122
val endpoint = (config.files(name) + slashIfNone(destRelativePath).toIriPath).toAkkaUri
110123
attributes(Put(endpoint, LinkFile(sourceRelativePath)).withCredentials)
111124
}
@@ -149,7 +162,8 @@ object StorageClient {
149162
cl: UntypedHttpClient[F],
150163
um: FromEntityUnmarshaller[A]
151164
): HttpClient[F, A] = new HttpClient[F, A] {
152-
private val logger = Logger(s"IamHttpClient[${implicitly[ClassTag[A]]}]")
165+
private val logger = Logger(s"IamHttpClient[${implicitly[ClassTag[A]]}]")
166+
private val emptyChunk = "An HttpEntity.Chunk must have non-empty data"
153167

154168
private def typeAndReason(string: String): Either[circe.Error, (String, String)] =
155169
parse(string).flatMap { json =>
@@ -158,8 +172,16 @@ object StorageClient {
158172
}
159173
}
160174

175+
private def handleError[B](req: HttpRequest): Throwable => F[B] = {
176+
case NonFatal(ex: IllegalArgumentException) if ex.getMessage != null && ex.getMessage.endsWith(emptyChunk) =>
177+
F.raiseError(EmptyChunk)
178+
case NonFatal(th) =>
179+
logger.error(s"Unexpected response for Storage call. Request: '${req.method} ${req.uri}'", th)
180+
F.raiseError(UnknownError(StatusCodes.InternalServerError, th.getMessage))
181+
}
182+
161183
override def apply(req: HttpRequest): F[A] =
162-
cl.apply(req).flatMap { resp =>
184+
cl(req).handleErrorWith(handleError(req)).flatMap { resp =>
163185
resp.status match {
164186
case StatusCodes.Unauthorized =>
165187
cl.toString(resp.entity).flatMap { entityAsString =>
@@ -179,12 +201,12 @@ object StorageClient {
179201
val value = L.liftIO(IO.fromFuture(IO(um(resp.entity))))
180202
value.recoverWith {
181203
case pf: ParsingFailure =>
182-
logger.error(
183-
s"Failed to parse a successful response of '${req.method.name()} ${req.getUri().toString}'.")
204+
logger
205+
.error(s"Failed to parse a successful response of '${req.method.name()} ${req.getUri().toString}'.")
184206
F.raiseError[A](UnmarshallingError(pf.getMessage()))
185207
case df: DecodingFailure =>
186-
logger.error(
187-
s"Failed to decode a successful response of '${req.method.name()} ${req.getUri().toString}'.")
208+
logger
209+
.error(s"Failed to decode a successful response of '${req.method.name()} ${req.getUri().toString}'.")
188210
F.raiseError(UnmarshallingError(df.getMessage()))
189211
}
190212

@@ -197,7 +219,8 @@ object StorageClient {
197219
case "PathAlreadyExists" => F.raiseError(InvalidPath(msg))
198220
case _ =>
199221
logger.error(
200-
s"Received '${other.value}' when accessing '${req.method.name()} ${req.uri.toString()}', response entity as string: '$msg'")
222+
s"Received '${other.value}' when accessing '${req.method.name()} ${req.uri.toString()}', response entity as string: '$msg'"
223+
)
201224
F.raiseError[A](UnknownError(other, msg))
202225

203226
}
@@ -222,11 +245,14 @@ object StorageClient {
222245
implicit val mt: ActorMaterializer = ActorMaterializer()
223246
implicit val ec: ExecutionContext = as.dispatcher
224247
implicit val ucl: UntypedHttpClient[F] = HttpClient.untyped[F]
225-
new StorageClient(config,
226-
httpClient[F, FileAttributes],
227-
httpClient[F, AkkaSource],
228-
httpClient[F, ServiceDescription],
229-
httpClient[F, NotUsed])
248+
new StorageClient(
249+
config,
250+
httpClient[F, FileAttributes],
251+
httpClient[F, Digest],
252+
httpClient[F, AkkaSource],
253+
httpClient[F, ServiceDescription],
254+
httpClient[F, NotUsed]
255+
)
230256
}
231257
}
232258
// $COVERAGE-ON$

client/src/main/scala/ch/epfl/bluebrain/nexus/storage/client/StorageClientError.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ object StorageClientError {
1515

1616
final case class UnmarshallingError[A: ClassTag](reason: String)
1717
extends StorageClientError(
18-
s"Unable to parse or decode the response from Storage to a '${implicitly[ClassTag[A]]}' due to '$reason'.")
18+
s"Unable to parse or decode the response from Storage to a '${implicitly[ClassTag[A]]}' due to '$reason'."
19+
)
1920

2021
final case class UnknownError(status: StatusCode, entityAsString: String)
2122
extends StorageClientError("The request did not complete successfully.")
2223

24+
final case object EmptyChunk extends StorageClientError("Chunk with empty data")
25+
2326
final case class NotFound(reason: String) extends StorageClientError(reason)
2427

2528
final case class InvalidPath(reason: String) extends StorageClientError(reason)

client/src/main/scala/ch/epfl/bluebrain/nexus/storage/client/config/StorageClientConfig.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ final case class StorageClientConfig(iri: AbsoluteIri, prefix: String) {
1919
*/
2020
def files(name: String): AbsoluteIri = buckets + name + "files"
2121

22+
/**
23+
* The digests endpoint: /buckets/{name}/digests
24+
*
25+
* @param name the storage bucket name
26+
*/
27+
def digests(name: String): AbsoluteIri = buckets + name + "digests"
28+
2229
}
2330

2431
object StorageClientConfig {

0 commit comments

Comments
 (0)