Skip to content

Commit addd554

Browse files
authored
Added /files/{uuids} endpoint (#188)
Signed-off-by: Arnau Mora Gras <[email protected]>
1 parent e81504f commit addd554

File tree

8 files changed

+189
-24
lines changed

8 files changed

+189
-24
lines changed

src/main/kotlin/server/endpoints/files/RequestFileEndpoint.kt

+15-11
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,28 @@ import kotlinx.coroutines.withContext
1111
import server.endpoints.EndpointBase
1212
import server.error.Errors
1313
import server.response.files.RequestFileResponseData
14+
import server.response.files.RequestFilesResponseData
1415
import server.response.respondFailure
1516
import server.response.respondSuccess
1617
import storage.HashUtils
1718
import storage.MessageDigestAlgorithm
1819
import storage.Storage
1920

21+
@Deprecated("This endpoint shall be removed once the new client is deployed")
2022
object RequestFileEndpoint : EndpointBase("/file/{uuids}") {
2123
private const val DEFAULT_HTTP_PORT = 80
2224

2325
private val digest = MessageDigest.getInstance(MessageDigestAlgorithm.SHA_256)
2426

25-
private suspend fun RoutingContext.getDataFor(uuid: String): RequestFileResponseData.Data {
27+
private suspend fun RoutingContext.getDataFor(uuid: String): RequestFilesResponseData.Data {
2628
val file = Storage.find(uuid) ?: throw FileNotFoundException("Could not find file with uuid $uuid")
2729
val downloadAddress = call.request.origin.let { p ->
2830
val port = p.serverPort.takeIf { it != DEFAULT_HTTP_PORT }?.let { ":$it" } ?: ""
2931
"${p.scheme}://${p.serverHost}$port/download/$uuid"
3032
}
3133
val size = withContext(Dispatchers.IO) { Files.size(file.toPath()) }
3234

33-
return RequestFileResponseData.Data(
35+
return RequestFilesResponseData.Data(
3436
uuid = uuid,
3537
hash = HashUtils.getCheckSumFromFile(digest, file),
3638
filename = file.name,
@@ -45,17 +47,19 @@ object RequestFileEndpoint : EndpointBase("/file/{uuids}") {
4547

4648
// It's impossible that "list" has size 0
4749
try {
48-
respondSuccess(
49-
data = if (list.size <= 1) {
50-
RequestFileResponseData(
51-
listOf(getDataFor(uuids))
50+
if (list.size <= 1) {
51+
respondSuccess(
52+
data = RequestFileResponseData(
53+
getDataFor(uuids)
5254
)
53-
} else {
54-
RequestFileResponseData(
55-
list.map { getDataFor(it) }
55+
)
56+
} else {
57+
respondSuccess(
58+
data = RequestFilesResponseData(
59+
files = list.map { getDataFor(it) }
5660
)
57-
}
58-
)
61+
)
62+
}
5963
} catch (_: FileNotFoundException) {
6064
respondFailure(Errors.FileNotFound)
6165
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package server.endpoints.files
2+
3+
import io.ktor.server.plugins.origin
4+
import io.ktor.server.routing.RoutingContext
5+
import io.ktor.server.util.getValue
6+
import java.io.FileNotFoundException
7+
import java.nio.file.Files
8+
import java.security.MessageDigest
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.withContext
11+
import server.endpoints.EndpointBase
12+
import server.error.Errors
13+
import server.response.files.RequestFilesResponseData
14+
import server.response.respondFailure
15+
import server.response.respondSuccess
16+
import storage.HashUtils
17+
import storage.MessageDigestAlgorithm
18+
import storage.Storage
19+
20+
object RequestFilesEndpoint : EndpointBase("/files/{uuids}") {
21+
private const val DEFAULT_HTTP_PORT = 80
22+
23+
private val digest = MessageDigest.getInstance(MessageDigestAlgorithm.SHA_256)
24+
25+
private suspend fun RoutingContext.getDataFor(uuid: String): RequestFilesResponseData.Data {
26+
val file = Storage.find(uuid) ?: throw FileNotFoundException("Could not find file with uuid $uuid")
27+
val downloadAddress = call.request.origin.let { p ->
28+
val port = p.serverPort.takeIf { it != DEFAULT_HTTP_PORT }?.let { ":$it" } ?: ""
29+
"${p.scheme}://${p.serverHost}$port/download/$uuid"
30+
}
31+
val size = withContext(Dispatchers.IO) { Files.size(file.toPath()) }
32+
33+
return RequestFilesResponseData.Data(
34+
uuid = uuid,
35+
hash = HashUtils.getCheckSumFromFile(digest, file),
36+
filename = file.name,
37+
download = downloadAddress,
38+
size = size
39+
)
40+
}
41+
42+
override suspend fun RoutingContext.endpoint() {
43+
val uuids: String by call.parameters
44+
val list = uuids.split(",")
45+
46+
// It's impossible that "list" has size 0
47+
try {
48+
respondSuccess(
49+
data = if (list.size <= 1) {
50+
RequestFilesResponseData(
51+
listOf(getDataFor(uuids))
52+
)
53+
} else {
54+
RequestFilesResponseData(
55+
list.map { getDataFor(it) }
56+
)
57+
}
58+
)
59+
} catch (_: FileNotFoundException) {
60+
respondFailure(Errors.FileNotFound)
61+
}
62+
}
63+
}

src/main/kotlin/server/plugins/Routing.kt

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import server.endpoints.delete.DeleteSectorEndpoint
2323
import server.endpoints.delete.DeleteZoneEndpoint
2424
import server.endpoints.files.DownloadFileEndpoint
2525
import server.endpoints.files.RequestFileEndpoint
26+
import server.endpoints.files.RequestFilesEndpoint
2627
import server.endpoints.info.ServerInfoEndpoint
2728
import server.endpoints.patch.PatchAreaEndpoint
2829
import server.endpoints.patch.PatchPathEndpoint
@@ -84,6 +85,7 @@ fun Application.configureEndpoints() {
8485
patch(PatchBlockEndpoint)
8586

8687
get(RequestFileEndpoint)
88+
get(RequestFilesEndpoint)
8789
get(DownloadFileEndpoint)
8890
}
8991
}

src/main/kotlin/server/response/files/RequestFileResponseData.kt

+12-9
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ import server.response.ResponseData
66

77
@KoverIgnore
88
@Serializable
9+
@Deprecated("This response shall be removed once the new client is deployed. See RequestFileEndpoint.")
910
data class RequestFileResponseData(
10-
val files: List<Data>
11+
val uuid: String,
12+
val hash: String,
13+
val filename: String,
14+
val download: String,
15+
val size: Long
1116
): ResponseData {
12-
@KoverIgnore
13-
@Serializable
14-
data class Data(
15-
val uuid: String,
16-
val hash: String,
17-
val filename: String,
18-
val download: String,
19-
val size: Long
17+
constructor(data: RequestFilesResponseData.Data) : this(
18+
data.uuid,
19+
data.hash,
20+
data.filename,
21+
data.download,
22+
data.size
2023
)
2124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package server.response.files
2+
3+
import KoverIgnore
4+
import kotlinx.serialization.Serializable
5+
import server.response.ResponseData
6+
7+
@KoverIgnore
8+
@Serializable
9+
data class RequestFilesResponseData(
10+
val files: List<Data>
11+
): ResponseData {
12+
@KoverIgnore
13+
@Serializable
14+
data class Data(
15+
val uuid: String,
16+
val hash: String,
17+
val filename: String,
18+
val download: String,
19+
val size: Long
20+
)
21+
}

src/test/kotlin/server/base/Patching.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import kotlinx.serialization.Serializable
2222
import kotlinx.serialization.encodeToString
2323
import server.base.ApplicationTestBase.Companion.AUTH_TOKEN
2424
import server.base.patch.PropertyValuePair
25-
import server.response.files.RequestFileResponseData
25+
import server.response.files.RequestFilesResponseData
2626
import storage.HashUtils
2727
import storage.MessageDigestAlgorithm
2828
import storage.Storage
@@ -155,8 +155,8 @@ fun <Type: BaseEntity> ApplicationTestBase.testPatchingFile(
155155

156156
// Only fetch file if the request was not a removal
157157
if (resourcePath != null) {
158-
get("/file/$elementFile").apply {
159-
assertSuccess<RequestFileResponseData> { response ->
158+
get("/files/$elementFile").apply {
159+
assertSuccess<RequestFilesResponseData> { response ->
160160
val data = response?.files?.first()
161161
assertNotNull(data)
162162
val serverHash = data.hash

src/test/kotlin/server/endpoints/files/TestFileFetching.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import server.DataProvider
1212
import server.base.ApplicationTestBase
1313
import server.error.Errors
1414
import server.response.files.RequestFileResponseData
15+
import server.response.files.RequestFilesResponseData
1516

1617
class TestFileFetching : ApplicationTestBase() {
1718
@Test
@@ -60,7 +61,7 @@ class TestFileFetching : ApplicationTestBase() {
6061
val zone: Zone = ServerDatabase.instance.query { Zone[zoneId] }
6162

6263
get("/file/${area.image.name},${zone.image.name}").apply {
63-
assertSuccess<RequestFileResponseData> { data ->
64+
assertSuccess<RequestFilesResponseData> { data ->
6465
assertNotNull(data)
6566

6667
val files = data.files
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package server.endpoints.files
2+
3+
import ServerDatabase
4+
import assertions.assertFailure
5+
import assertions.assertSuccess
6+
import database.entity.Area
7+
import database.entity.Zone
8+
import server.DataProvider
9+
import server.base.ApplicationTestBase
10+
import server.error.Errors
11+
import server.response.files.RequestFilesResponseData
12+
import kotlin.test.Test
13+
import kotlin.test.assertNotNull
14+
import kotlin.test.assertTrue
15+
16+
class TestFilesFetching : ApplicationTestBase() {
17+
@Test
18+
fun `test data`() = test {
19+
val areaId = DataProvider.provideSampleArea(this)
20+
assertNotNull(areaId)
21+
22+
val area: Area = ServerDatabase.instance.query { Area[areaId] }
23+
24+
get("/files/${area.image.name}").apply {
25+
assertSuccess<RequestFilesResponseData> { data ->
26+
assertNotNull(data)
27+
}
28+
}
29+
}
30+
31+
@Test
32+
fun `test data no extension`() = test {
33+
val areaId = DataProvider.provideSampleArea(this)
34+
assertNotNull(areaId)
35+
36+
val area: Area = ServerDatabase.instance.query { Area[areaId] }
37+
38+
get("/files/${area.image.nameWithoutExtension}").apply {
39+
assertSuccess<RequestFilesResponseData> { data ->
40+
assertNotNull(data)
41+
}
42+
}
43+
}
44+
45+
@Test
46+
fun `test doesn't exist`() = test {
47+
get("/files/unknown").apply {
48+
assertFailure(Errors.FileNotFound)
49+
}
50+
}
51+
52+
@Test
53+
fun `test data multiple`() = test {
54+
val areaId = DataProvider.provideSampleArea(this)
55+
assertNotNull(areaId)
56+
val zoneId = DataProvider.provideSampleZone(this, areaId)
57+
assertNotNull(zoneId)
58+
59+
val area: Area = ServerDatabase.instance.query { Area[areaId] }
60+
val zone: Zone = ServerDatabase.instance.query { Zone[zoneId] }
61+
62+
get("/files/${area.image.name},${zone.image.name}").apply {
63+
assertSuccess<RequestFilesResponseData> { data ->
64+
assertNotNull(data)
65+
66+
val files = data.files
67+
assertTrue(files.isNotEmpty())
68+
}
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)