Skip to content

Commit fd68274

Browse files
authored
Add GPX files to Sectors (#109)
Signed-off-by: Arnau Mora <[email protected]>
1 parent f48e9f2 commit fd68274

File tree

11 files changed

+256
-7
lines changed

11 files changed

+256
-7
lines changed

config/detekt/detekt.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ complexity:
128128
threshold: 600
129129
LongMethod:
130130
active: true
131-
threshold: 60
131+
threshold: 80
132132
LongParameterList:
133133
active: true
134134
functionThreshold: 6

src/main/kotlin/database/entity/Sector.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ class Sector(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
3535
get() = File(Storage.ImagesDir, _image)
3636
set(value) { _image = value.toRelativeString(Storage.ImagesDir) }
3737

38+
var gpx: File?
39+
get() = _gpx?.let { File(Storage.TracksDir, it) }
40+
set(value) { _gpx = value?.toRelativeString(Storage.TracksDir) }
41+
3842
var point: LatLng?
3943
get() = _latitude?.let { lat -> _longitude?.let { lon -> LatLng(lat, lon) } }
4044
set(value) { _latitude = value?.latitude; _longitude = value?.longitude }
4145

4246
var zone by Zone referencedOn Sectors.zone
4347

4448
private var _image: String by Sectors.imagePath
49+
private var _gpx: String? by Sectors.gpxPath
4550

4651
private var _latitude: Double? by Sectors.latitude
4752
private var _longitude: Double? by Sectors.longitude
@@ -61,6 +66,7 @@ class Sector(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
6166
* - `sun_time`: [sunTime] ([SunTime])
6267
* - `walking_time`: [walkingTime] ([Int]|`null`)
6368
* - `image`: [image] ([String])
69+
* - `gpx`: [gpx] ([String])
6470
* - `point`: [point] ([String])
6571
* - `zone_id`: [zone] ([Int])
6672
*
@@ -74,6 +80,7 @@ class Sector(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
7480
"sun_time" to sunTime,
7581
"walking_time" to walkingTime,
7682
"image" to _image.substringBeforeLast('.'),
83+
"gpx" to _gpx?.substringBeforeLast('.'),
7784
"point" to point,
7885
"weight" to weight,
7986
"zone_id" to zone.id.value
@@ -107,6 +114,7 @@ class Sector(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
107114
result = 31 * result + sunTime.hashCode()
108115
result = 31 * result + (walkingTime?.hashCode() ?: 0)
109116
result = 31 * result + image.hashCode()
117+
result = 31 * result + (gpx?.hashCode() ?: 0)
110118
result = 31 * result + (point?.hashCode() ?: 0)
111119
result = 31 * result + (weight.hashCode())
112120
result = 31 * result + zone.hashCode()

src/main/kotlin/database/table/Sectors.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ object Sectors : BaseTable() {
77
val displayName = varchar("display_name", SqlConsts.DISPLAY_NAME_LENGTH)
88

99
val imagePath = varchar("image", SqlConsts.FILE_LENGTH)
10+
val gpxPath = varchar("gpx", SqlConsts.FILE_LENGTH).nullable()
1011

1112
val latitude = double("latitude").nullable()
1213
val longitude = double("longitude").nullable()

src/main/kotlin/server/endpoints/create/NewSectorEndpoint.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") {
3131
var zone: Zone? = null
3232

3333
var imageFile: File? = null
34+
var gpxFile: File? = null
3435

3536
receiveMultipart(
3637
forEachFormItem = { partData ->
@@ -42,25 +43,27 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") {
4243
"walkingTime" -> walkingTime = partData.value.toUIntOrNull()
4344
"weight" -> weight = partData.value
4445
"zone" -> ServerDatabase.instance.query {
45-
zone = Zone.findById(partData.value.toInt())
46-
?: return@query respondFailure(ParentNotFound)
46+
zone = Zone.findById(partData.value.toInt()) ?: return@query respondFailure(ParentNotFound)
4747
}
4848
}
4949
},
5050
forEachFileItem = { partData ->
5151
when (partData.name) {
5252
"image" -> imageFile = partData.save(Storage.ImagesDir)
53+
"gpx" -> gpxFile = partData.save(Storage.TracksDir)
5354
}
5455
}
5556
)
5657

5758
if (isAnyNull(displayName, imageFile, kidsApt, sunTime, zone)) {
5859
imageFile?.delete()
60+
gpxFile?.delete()
5961
return respondFailure(
6062
MissingData,
6163
jsonOf(
6264
"multipart" to rawMultipartFormItems,
63-
"imageFile" to imageFile?.path
65+
"imageFile" to imageFile?.path,
66+
"gpxFile" to gpxFile?.path
6467
).toString()
6568
)
6669
}
@@ -73,6 +76,7 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") {
7376
this.sunTime = sunTime!!
7477
this.walkingTime = walkingTime
7578
this.image = imageFile!!
79+
this.gpx = gpxFile
7680
weight?.let { this.weight = it }
7781
this.zone = zone!!
7882
}.toJson()

src/main/kotlin/server/endpoints/patch/PatchSectorEndpoint.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") {
3333
?: return respondFailure(Errors.ObjectNotFound)
3434

3535
// Nullable types: point, walkingTime
36+
// Nullable files: gpxFile
3637

3738
var displayName: String? = null
3839
var point: LatLng? = null
@@ -46,6 +47,7 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") {
4647
var removeWalkingTime = false
4748

4849
var imageFile: File? = null
50+
var gpxFile: File? = null
4951

5052
receiveMultipart(
5153
forEachFormItem = { partData ->
@@ -78,11 +80,15 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") {
7880
val uuid = ServerDatabase.instance.query { sector.image.nameWithoutExtension }
7981
imageFile = partData.save(Storage.ImagesDir, UUID.fromString(uuid))
8082
}
83+
"gpx" -> {
84+
val uuid = ServerDatabase.instance.query { sector.gpx?.nameWithoutExtension }
85+
gpxFile = partData.save(Storage.TracksDir, uuid?.let(UUID::fromString) ?: UUID.randomUUID())
86+
}
8187
}
8288
}
8389
)
8490

85-
if (areAllNull(displayName, imageFile, kidsApt, point, sunTime, walkingTime, weight, zone) &&
91+
if (areAllNull(displayName, imageFile, gpxFile, kidsApt, point, sunTime, walkingTime, weight, zone) &&
8692
areAllFalse(removePoint, removeWalkingTime)
8793
) {
8894
return respondSuccess(httpStatusCode = HttpStatusCode.NoContent)

src/main/kotlin/server/request/MultipartExtensions.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import java.util.UUID
1515
*
1616
* @throws IOException If there's a problem while writing to the file system.
1717
*/
18-
fun PartData.FileItem.save(rootDir: File, uuid: UUID = UUID.randomUUID(), overwrite: Boolean = true): File {
18+
fun PartData.FileItem.save(rootDir: File, uuid: UUID? = null, overwrite: Boolean = true): File {
1919
rootDir.mkdirs()
2020

2121
val fileExtension = originalFileName?.takeLastWhile { it != '.' }
22-
val fileName = "$uuid.$fileExtension"
22+
val fileName = "${uuid ?: UUID.randomUUID()}.$fileExtension"
2323
val targetFile = File(rootDir, fileName)
2424

2525
if (overwrite && targetFile.exists()) {

src/test/kotlin/assertions/ResultAssertions.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package assertions
22

33
import io.ktor.client.statement.HttpResponse
44
import io.ktor.client.statement.bodyAsText
5+
import io.ktor.client.statement.request
56
import io.ktor.http.HttpStatusCode
67
import kotlin.test.assertEquals
78
import kotlin.test.assertFalse
@@ -34,6 +35,7 @@ suspend inline fun HttpResponse.assertSuccess(
3435
status,
3536
StringBuilder().apply {
3637
appendLine("expected: $statusCode but was: $status")
38+
appendLine("Url: ${request.url}")
3739
if (errorMessage != null) {
3840
appendLine("Message: $errorMessage")
3941
}

src/test/kotlin/server/DataProvider.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ object DataProvider {
161161
skipKidsApt: Boolean = false,
162162
skipSunTime: Boolean = false,
163163
skipImage: Boolean = false,
164+
skipGpx: Boolean = false,
164165
assertion: suspend HttpResponse.() -> Int? = {
165166
var sectorId: Int? = null
166167
assertSuccess(HttpStatusCode.Created) { data ->
@@ -174,6 +175,9 @@ object DataProvider {
174175
val image = this::class.java.getResourceAsStream("/images/desploms1.jpg")!!.use {
175176
it.readBytes()
176177
}
178+
val gpx = this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!!.use {
179+
it.readBytes()
180+
}
177181

178182
var sectorId: Int?
179183

@@ -195,6 +199,11 @@ object DataProvider {
195199
append(HttpHeaders.ContentType, "image/jpeg")
196200
append(HttpHeaders.ContentDisposition, "filename=sector.jpg")
197201
})
202+
if (!skipGpx)
203+
append("gpx", gpx, Headers.build {
204+
append(HttpHeaders.ContentType, "application/gpx+xml")
205+
append(HttpHeaders.ContentDisposition, "filename=sector.gpx")
206+
})
198207
}
199208
) {
200209
header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN")

src/test/kotlin/server/endpoints/create/TestSectorCreationEndpoint.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlin.test.Test
88
import kotlin.test.assertEquals
99
import kotlin.test.assertNotEquals
1010
import kotlin.test.assertNotNull
11+
import kotlin.test.assertNull
1112
import kotlin.test.assertTrue
1213
import server.DataProvider
1314
import server.base.ApplicationTestBase
@@ -39,6 +40,42 @@ class TestSectorCreationEndpoint: ApplicationTestBase() {
3940
val imageFile = sector.image
4041
assertTrue(imageFile.exists())
4142

43+
val gpxFile = sector.gpx
44+
assertNotNull(gpxFile)
45+
assertTrue(gpxFile.exists())
46+
47+
assertNotEquals(LastUpdate.get(), lastUpdate)
48+
}
49+
}
50+
51+
@Test
52+
fun `test sector creation - without gpx`() = test {
53+
val lastUpdate = ServerDatabase.instance.query { LastUpdate.get() }
54+
55+
val areaId: Int? = DataProvider.provideSampleArea()
56+
assertNotNull(areaId)
57+
58+
val zoneId: Int? = DataProvider.provideSampleZone(areaId)
59+
assertNotNull(zoneId)
60+
61+
val sectorId: Int? = DataProvider.provideSampleSector(zoneId, skipGpx = true)
62+
assertNotNull(sectorId)
63+
64+
ServerDatabase.instance.query {
65+
val sector = Sector[sectorId]
66+
assertNotNull(sector)
67+
assertEquals(DataProvider.SampleSector.displayName, sector.displayName)
68+
assertEquals(DataProvider.SampleSector.point, sector.point)
69+
assertEquals(DataProvider.SampleSector.kidsApt, sector.kidsApt)
70+
assertEquals(DataProvider.SampleSector.walkingTime, sector.walkingTime)
71+
assertEquals(DataProvider.SampleSector.sunTime, sector.sunTime)
72+
73+
val imageFile = sector.image
74+
assertTrue(imageFile.exists())
75+
76+
val gpxFile = sector.gpx
77+
assertNull(gpxFile)
78+
4279
assertNotEquals(LastUpdate.get(), lastUpdate)
4380
}
4481
}

src/test/kotlin/server/endpoints/patch/TestPatchSectorEndpoint.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,64 @@ class TestPatchSectorEndpoint : ApplicationTestBase() {
276276
}
277277
}
278278

279+
@Test
280+
fun `test patching Sector - update gpx`() = test {
281+
val areaId = DataProvider.provideSampleArea()
282+
assertNotNull(areaId)
283+
284+
val zoneId = DataProvider.provideSampleZone(areaId)
285+
assertNotNull(zoneId)
286+
287+
val sectorId = DataProvider.provideSampleSector(zoneId)
288+
assertNotNull(sectorId)
289+
290+
val oldTimestamp = ServerDatabase.instance.query { Sector[sectorId].timestamp }
291+
292+
val gpx = this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!!.use {
293+
it.readBytes()
294+
}
295+
296+
client.submitFormWithBinaryData(
297+
url = "/sector/$sectorId",
298+
formData = formData {
299+
append("gpx", gpx, Headers.build {
300+
append(HttpHeaders.ContentType, "application/gpx+xml")
301+
append(HttpHeaders.ContentDisposition, "filename=sector.gpx")
302+
})
303+
}
304+
) {
305+
header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN")
306+
}.apply {
307+
assertSuccess()
308+
}
309+
310+
var sectorGpx: String? = null
311+
312+
ServerDatabase.instance.query {
313+
val sector = Sector[sectorId]
314+
assertNotNull(sector)
315+
316+
val gpxFile = sector.gpx
317+
assertNotNull(gpxFile)
318+
sectorGpx = gpxFile.toRelativeString(Storage.TracksDir)
319+
assertTrue(gpxFile.exists())
320+
321+
assertNotEquals(oldTimestamp, sector.timestamp)
322+
}
323+
324+
get("/file/$sectorGpx").apply {
325+
assertSuccess { data ->
326+
assertNotNull(data)
327+
val serverHash = data.getString("hash")
328+
val localHash = HashUtils.getCheckSumFromStream(
329+
MessageDigest.getInstance(MessageDigestAlgorithm.SHA_256),
330+
this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!!
331+
)
332+
assertEquals(localHash, serverHash)
333+
}
334+
}
335+
}
336+
279337
@Test
280338
fun `test patching Sector - remove walking time`() = test {
281339
val areaId = DataProvider.provideSampleArea()
@@ -304,6 +362,8 @@ class TestPatchSectorEndpoint : ApplicationTestBase() {
304362
val sector = Sector[sectorId]
305363
assertNotNull(sector)
306364
assertNull(sector.walkingTime)
365+
366+
assertNotEquals(oldTimestamp, sector.timestamp)
307367
}
308368
}
309369

0 commit comments

Comments
 (0)