diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 7010e64..cb834f8 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -128,7 +128,7 @@ complexity: threshold: 600 LongMethod: active: true - threshold: 60 + threshold: 80 LongParameterList: active: true functionThreshold: 6 diff --git a/src/main/kotlin/database/entity/Sector.kt b/src/main/kotlin/database/entity/Sector.kt index a11382c..ddb16ff 100644 --- a/src/main/kotlin/database/entity/Sector.kt +++ b/src/main/kotlin/database/entity/Sector.kt @@ -35,6 +35,10 @@ class Sector(id: EntityID): BaseEntity(id), JsonSerializable { get() = File(Storage.ImagesDir, _image) set(value) { _image = value.toRelativeString(Storage.ImagesDir) } + var gpx: File? + get() = _gpx?.let { File(Storage.TracksDir, it) } + set(value) { _gpx = value?.toRelativeString(Storage.TracksDir) } + var point: LatLng? get() = _latitude?.let { lat -> _longitude?.let { lon -> LatLng(lat, lon) } } set(value) { _latitude = value?.latitude; _longitude = value?.longitude } @@ -42,6 +46,7 @@ class Sector(id: EntityID): BaseEntity(id), JsonSerializable { var zone by Zone referencedOn Sectors.zone private var _image: String by Sectors.imagePath + private var _gpx: String? by Sectors.gpxPath private var _latitude: Double? by Sectors.latitude private var _longitude: Double? by Sectors.longitude @@ -61,6 +66,7 @@ class Sector(id: EntityID): BaseEntity(id), JsonSerializable { * - `sun_time`: [sunTime] ([SunTime]) * - `walking_time`: [walkingTime] ([Int]|`null`) * - `image`: [image] ([String]) + * - `gpx`: [gpx] ([String]) * - `point`: [point] ([String]) * - `zone_id`: [zone] ([Int]) * @@ -74,6 +80,7 @@ class Sector(id: EntityID): BaseEntity(id), JsonSerializable { "sun_time" to sunTime, "walking_time" to walkingTime, "image" to _image.substringBeforeLast('.'), + "gpx" to _gpx?.substringBeforeLast('.'), "point" to point, "weight" to weight, "zone_id" to zone.id.value @@ -107,6 +114,7 @@ class Sector(id: EntityID): BaseEntity(id), JsonSerializable { result = 31 * result + sunTime.hashCode() result = 31 * result + (walkingTime?.hashCode() ?: 0) result = 31 * result + image.hashCode() + result = 31 * result + (gpx?.hashCode() ?: 0) result = 31 * result + (point?.hashCode() ?: 0) result = 31 * result + (weight.hashCode()) result = 31 * result + zone.hashCode() diff --git a/src/main/kotlin/database/table/Sectors.kt b/src/main/kotlin/database/table/Sectors.kt index dfa3336..411a207 100644 --- a/src/main/kotlin/database/table/Sectors.kt +++ b/src/main/kotlin/database/table/Sectors.kt @@ -7,6 +7,7 @@ object Sectors : BaseTable() { val displayName = varchar("display_name", SqlConsts.DISPLAY_NAME_LENGTH) val imagePath = varchar("image", SqlConsts.FILE_LENGTH) + val gpxPath = varchar("gpx", SqlConsts.FILE_LENGTH).nullable() val latitude = double("latitude").nullable() val longitude = double("longitude").nullable() diff --git a/src/main/kotlin/server/endpoints/create/NewSectorEndpoint.kt b/src/main/kotlin/server/endpoints/create/NewSectorEndpoint.kt index a7f7a8b..4348f45 100644 --- a/src/main/kotlin/server/endpoints/create/NewSectorEndpoint.kt +++ b/src/main/kotlin/server/endpoints/create/NewSectorEndpoint.kt @@ -31,6 +31,7 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") { var zone: Zone? = null var imageFile: File? = null + var gpxFile: File? = null receiveMultipart( forEachFormItem = { partData -> @@ -42,25 +43,27 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") { "walkingTime" -> walkingTime = partData.value.toUIntOrNull() "weight" -> weight = partData.value "zone" -> ServerDatabase.instance.query { - zone = Zone.findById(partData.value.toInt()) - ?: return@query respondFailure(ParentNotFound) + zone = Zone.findById(partData.value.toInt()) ?: return@query respondFailure(ParentNotFound) } } }, forEachFileItem = { partData -> when (partData.name) { "image" -> imageFile = partData.save(Storage.ImagesDir) + "gpx" -> gpxFile = partData.save(Storage.TracksDir) } } ) if (isAnyNull(displayName, imageFile, kidsApt, sunTime, zone)) { imageFile?.delete() + gpxFile?.delete() return respondFailure( MissingData, jsonOf( "multipart" to rawMultipartFormItems, - "imageFile" to imageFile?.path + "imageFile" to imageFile?.path, + "gpxFile" to gpxFile?.path ).toString() ) } @@ -73,6 +76,7 @@ object NewSectorEndpoint : SecureEndpointBase("/sector") { this.sunTime = sunTime!! this.walkingTime = walkingTime this.image = imageFile!! + this.gpx = gpxFile weight?.let { this.weight = it } this.zone = zone!! }.toJson() diff --git a/src/main/kotlin/server/endpoints/patch/PatchSectorEndpoint.kt b/src/main/kotlin/server/endpoints/patch/PatchSectorEndpoint.kt index 341e134..5b62214 100644 --- a/src/main/kotlin/server/endpoints/patch/PatchSectorEndpoint.kt +++ b/src/main/kotlin/server/endpoints/patch/PatchSectorEndpoint.kt @@ -33,6 +33,7 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") { ?: return respondFailure(Errors.ObjectNotFound) // Nullable types: point, walkingTime + // Nullable files: gpxFile var displayName: String? = null var point: LatLng? = null @@ -46,6 +47,7 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") { var removeWalkingTime = false var imageFile: File? = null + var gpxFile: File? = null receiveMultipart( forEachFormItem = { partData -> @@ -78,11 +80,15 @@ object PatchSectorEndpoint : SecureEndpointBase("/sector/{sectorId}") { val uuid = ServerDatabase.instance.query { sector.image.nameWithoutExtension } imageFile = partData.save(Storage.ImagesDir, UUID.fromString(uuid)) } + "gpx" -> { + val uuid = ServerDatabase.instance.query { sector.gpx?.nameWithoutExtension } + gpxFile = partData.save(Storage.TracksDir, uuid?.let(UUID::fromString) ?: UUID.randomUUID()) + } } } ) - if (areAllNull(displayName, imageFile, kidsApt, point, sunTime, walkingTime, weight, zone) && + if (areAllNull(displayName, imageFile, gpxFile, kidsApt, point, sunTime, walkingTime, weight, zone) && areAllFalse(removePoint, removeWalkingTime) ) { return respondSuccess(httpStatusCode = HttpStatusCode.NoContent) diff --git a/src/main/kotlin/server/request/MultipartExtensions.kt b/src/main/kotlin/server/request/MultipartExtensions.kt index 79403b5..32cecb9 100644 --- a/src/main/kotlin/server/request/MultipartExtensions.kt +++ b/src/main/kotlin/server/request/MultipartExtensions.kt @@ -15,11 +15,11 @@ import java.util.UUID * * @throws IOException If there's a problem while writing to the file system. */ -fun PartData.FileItem.save(rootDir: File, uuid: UUID = UUID.randomUUID(), overwrite: Boolean = true): File { +fun PartData.FileItem.save(rootDir: File, uuid: UUID? = null, overwrite: Boolean = true): File { rootDir.mkdirs() val fileExtension = originalFileName?.takeLastWhile { it != '.' } - val fileName = "$uuid.$fileExtension" + val fileName = "${uuid ?: UUID.randomUUID()}.$fileExtension" val targetFile = File(rootDir, fileName) if (overwrite && targetFile.exists()) { diff --git a/src/test/kotlin/assertions/ResultAssertions.kt b/src/test/kotlin/assertions/ResultAssertions.kt index 01347a8..399f345 100644 --- a/src/test/kotlin/assertions/ResultAssertions.kt +++ b/src/test/kotlin/assertions/ResultAssertions.kt @@ -2,6 +2,7 @@ package assertions import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request import io.ktor.http.HttpStatusCode import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -34,6 +35,7 @@ suspend inline fun HttpResponse.assertSuccess( status, StringBuilder().apply { appendLine("expected: $statusCode but was: $status") + appendLine("Url: ${request.url}") if (errorMessage != null) { appendLine("Message: $errorMessage") } diff --git a/src/test/kotlin/server/DataProvider.kt b/src/test/kotlin/server/DataProvider.kt index 97d9147..e249d1d 100644 --- a/src/test/kotlin/server/DataProvider.kt +++ b/src/test/kotlin/server/DataProvider.kt @@ -161,6 +161,7 @@ object DataProvider { skipKidsApt: Boolean = false, skipSunTime: Boolean = false, skipImage: Boolean = false, + skipGpx: Boolean = false, assertion: suspend HttpResponse.() -> Int? = { var sectorId: Int? = null assertSuccess(HttpStatusCode.Created) { data -> @@ -174,6 +175,9 @@ object DataProvider { val image = this::class.java.getResourceAsStream("/images/desploms1.jpg")!!.use { it.readBytes() } + val gpx = this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!!.use { + it.readBytes() + } var sectorId: Int? @@ -195,6 +199,11 @@ object DataProvider { append(HttpHeaders.ContentType, "image/jpeg") append(HttpHeaders.ContentDisposition, "filename=sector.jpg") }) + if (!skipGpx) + append("gpx", gpx, Headers.build { + append(HttpHeaders.ContentType, "application/gpx+xml") + append(HttpHeaders.ContentDisposition, "filename=sector.gpx") + }) } ) { header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN") diff --git a/src/test/kotlin/server/endpoints/create/TestSectorCreationEndpoint.kt b/src/test/kotlin/server/endpoints/create/TestSectorCreationEndpoint.kt index 8c8aabd..289b621 100644 --- a/src/test/kotlin/server/endpoints/create/TestSectorCreationEndpoint.kt +++ b/src/test/kotlin/server/endpoints/create/TestSectorCreationEndpoint.kt @@ -8,6 +8,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue import server.DataProvider import server.base.ApplicationTestBase @@ -39,6 +40,42 @@ class TestSectorCreationEndpoint: ApplicationTestBase() { val imageFile = sector.image assertTrue(imageFile.exists()) + val gpxFile = sector.gpx + assertNotNull(gpxFile) + assertTrue(gpxFile.exists()) + + assertNotEquals(LastUpdate.get(), lastUpdate) + } + } + + @Test + fun `test sector creation - without gpx`() = test { + val lastUpdate = ServerDatabase.instance.query { LastUpdate.get() } + + val areaId: Int? = DataProvider.provideSampleArea() + assertNotNull(areaId) + + val zoneId: Int? = DataProvider.provideSampleZone(areaId) + assertNotNull(zoneId) + + val sectorId: Int? = DataProvider.provideSampleSector(zoneId, skipGpx = true) + assertNotNull(sectorId) + + ServerDatabase.instance.query { + val sector = Sector[sectorId] + assertNotNull(sector) + assertEquals(DataProvider.SampleSector.displayName, sector.displayName) + assertEquals(DataProvider.SampleSector.point, sector.point) + assertEquals(DataProvider.SampleSector.kidsApt, sector.kidsApt) + assertEquals(DataProvider.SampleSector.walkingTime, sector.walkingTime) + assertEquals(DataProvider.SampleSector.sunTime, sector.sunTime) + + val imageFile = sector.image + assertTrue(imageFile.exists()) + + val gpxFile = sector.gpx + assertNull(gpxFile) + assertNotEquals(LastUpdate.get(), lastUpdate) } } diff --git a/src/test/kotlin/server/endpoints/patch/TestPatchSectorEndpoint.kt b/src/test/kotlin/server/endpoints/patch/TestPatchSectorEndpoint.kt index edf797d..b7446a2 100644 --- a/src/test/kotlin/server/endpoints/patch/TestPatchSectorEndpoint.kt +++ b/src/test/kotlin/server/endpoints/patch/TestPatchSectorEndpoint.kt @@ -276,6 +276,64 @@ class TestPatchSectorEndpoint : ApplicationTestBase() { } } + @Test + fun `test patching Sector - update gpx`() = test { + val areaId = DataProvider.provideSampleArea() + assertNotNull(areaId) + + val zoneId = DataProvider.provideSampleZone(areaId) + assertNotNull(zoneId) + + val sectorId = DataProvider.provideSampleSector(zoneId) + assertNotNull(sectorId) + + val oldTimestamp = ServerDatabase.instance.query { Sector[sectorId].timestamp } + + val gpx = this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!!.use { + it.readBytes() + } + + client.submitFormWithBinaryData( + url = "/sector/$sectorId", + formData = formData { + append("gpx", gpx, Headers.build { + append(HttpHeaders.ContentType, "application/gpx+xml") + append(HttpHeaders.ContentDisposition, "filename=sector.gpx") + }) + } + ) { + header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN") + }.apply { + assertSuccess() + } + + var sectorGpx: String? = null + + ServerDatabase.instance.query { + val sector = Sector[sectorId] + assertNotNull(sector) + + val gpxFile = sector.gpx + assertNotNull(gpxFile) + sectorGpx = gpxFile.toRelativeString(Storage.TracksDir) + assertTrue(gpxFile.exists()) + + assertNotEquals(oldTimestamp, sector.timestamp) + } + + get("/file/$sectorGpx").apply { + assertSuccess { data -> + assertNotNull(data) + val serverHash = data.getString("hash") + val localHash = HashUtils.getCheckSumFromStream( + MessageDigest.getInstance(MessageDigestAlgorithm.SHA_256), + this::class.java.getResourceAsStream("/tracks/ulldelmoro.gpx")!! + ) + assertEquals(localHash, serverHash) + } + } + } + @Test fun `test patching Sector - remove walking time`() = test { val areaId = DataProvider.provideSampleArea() @@ -304,6 +362,8 @@ class TestPatchSectorEndpoint : ApplicationTestBase() { val sector = Sector[sectorId] assertNotNull(sector) assertNull(sector.walkingTime) + + assertNotEquals(oldTimestamp, sector.timestamp) } } diff --git a/src/test/resources/tracks/ulldelmoro.gpx b/src/test/resources/tracks/ulldelmoro.gpx new file mode 100644 index 0000000..94bb1d2 --- /dev/null +++ b/src/test/resources/tracks/ulldelmoro.gpx @@ -0,0 +1,122 @@ + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + 1 + + + 0 + 1 + + + 0 + 2 + + + 0 + 3 + + + 0 + 4 + + + 0 + 5 + + + 0 + 6 + + + 0 + 7 + + + 0 + 8 + + + 0 + 9 + + + 0 + 10 + + + 0 + 11 + + + 0 + 12 + + + 0 + 13 + + + 0 + 14 + + + 0 + 15 + + + 0 + 16 + + + 0 + 17 + + + 0 + 18 + + + 0 + 19 + + + 0 + 20 + + + 0 + 21 + + + 0 + 22 + + + + + + + \ No newline at end of file