Skip to content

Commit a0a1b75

Browse files
authored
Auxiliar images for paths (#28)
Signed-off-by: Arnau Mora <[email protected]>
1 parent 50fec52 commit a0a1b75

File tree

10 files changed

+407
-37
lines changed

10 files changed

+407
-37
lines changed

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/database/entity/Path.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.Ending
55
import com.arnyminerz.escalaralcoiaicomtat.backend.data.GradeValue
66
import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
77
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
8+
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
89
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
910
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonArray
1011
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonOf
12+
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.mapJsonPrimitive
1113
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialization.JsonSerializable
1214
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialize
1315
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.toJson
16+
import java.io.File
1417
import java.time.Instant
1518
import org.jetbrains.annotations.VisibleForTesting
1619
import org.jetbrains.exposed.dao.IntEntityClass
@@ -90,6 +93,10 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
9093
get() = _reBuilder?.jsonArray?.serialize(Builder)
9194
set(value) { _reBuilder = value?.toJson()?.toString() }
9295

96+
var images: List<File>?
97+
get() = _images?.split('\n')?.map { File(Storage.ImagesDir, it) }
98+
set(value) { _images = value?.joinToString("\n") { it.toRelativeString(Storage.ImagesDir) } }
99+
93100
var sector by Sector referencedOn Paths.sector
94101

95102

@@ -107,6 +114,8 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
107114
var _builder: String? by Paths.builder
108115
private var _reBuilder: String? by Paths.reBuilder
109116

117+
private var _images: String? by Paths.images
118+
110119
override fun toJson(): JSONObject = jsonOf(
111120
"id" to id.value,
112121
"timestamp" to timestamp.toEpochMilli(),
@@ -141,6 +150,8 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
141150
"builder" to builder,
142151
"re_builder" to reBuilder,
143152

153+
"images" to images?.mapJsonPrimitive { it.toRelativeString(Storage.ImagesDir) },
154+
144155
"sector_id" to sector.id.value
145156
)
146157

@@ -178,6 +189,7 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
178189
result = 31 * result + (description?.hashCode() ?: 0)
179190
result = 31 * result + (builder?.hashCode() ?: 0)
180191
result = 31 * result + (reBuilder?.hashCode() ?: 0)
192+
result = 31 * result + (_images?.hashCode() ?: 0)
181193
result = 31 * result + sector.hashCode()
182194
return result
183195
}

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/database/table/Paths.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package com.arnyminerz.escalaralcoiaicomtat.backend.database.table
33
import com.arnyminerz.escalaralcoiaicomtat.backend.database.SqlConsts
44

55
object Paths: BaseTable() {
6+
/**
7+
* The maximum amount of images allowed in a path.
8+
*/
9+
const val MAX_IMAGES = 10
10+
611
val displayName = varchar("display_name", SqlConsts.DISPLAY_NAME_LENGTH)
712
val sketchId = uinteger("sketch_id")
813

@@ -33,5 +38,7 @@ object Paths: BaseTable() {
3338
val builder = varchar("builder", SqlConsts.BUILDER_LENGTH).nullable()
3439
val reBuilder = varchar("re_builder", SqlConsts.RE_BUILDER_LENGTH).nullable()
3540

41+
val images = varchar("images", SqlConsts.FILE_LENGTH * MAX_IMAGES).nullable().default(null)
42+
3643
val sector = reference("sector", Sectors)
3744
}

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/server/endpoints/create/NewPathEndpoint.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
88
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Path
99
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Sector
1010
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.info.LastUpdate
11+
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
1112
import com.arnyminerz.escalaralcoiaicomtat.backend.localization.Localization
1213
import com.arnyminerz.escalaralcoiaicomtat.backend.server.endpoints.SecureEndpointBase
1314
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors
15+
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.CouldNotClear
1416
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.MissingData
17+
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.TooManyImages
18+
import com.arnyminerz.escalaralcoiaicomtat.backend.server.request.save
1519
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondFailure
1620
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondSuccess
21+
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
1722
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
1823
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonArray
1924
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonOf
2025
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialize
2126
import io.ktor.http.HttpStatusCode
2227
import io.ktor.server.application.ApplicationCall
2328
import io.ktor.util.pipeline.PipelineContext
29+
import java.io.File
2430

2531
object NewPathEndpoint : SecureEndpointBase() {
2632
/** The number of different count properties. */
@@ -59,6 +65,8 @@ object NewPathEndpoint : SecureEndpointBase() {
5965
val reBuilder: MutableList<Builder> = mutableListOf()
6066
var sector: Sector? = null
6167

68+
var imageFiles: List<File>? = null
69+
6270
receiveMultipart(
6371
forEachFormItem = { partData ->
6472
when (partData.name) {
@@ -97,6 +105,13 @@ object NewPathEndpoint : SecureEndpointBase() {
97105
?: return@query respondFailure(Errors.ParentNotFound)
98106
}
99107
}
108+
},
109+
forEachFileItem = { partData ->
110+
when (partData.name) {
111+
"image" -> imageFiles = (imageFiles ?: emptyList())
112+
.toMutableList()
113+
.apply { add(partData.save(Storage.ImagesDir)) }
114+
}
100115
}
101116
)
102117

@@ -106,6 +121,18 @@ object NewPathEndpoint : SecureEndpointBase() {
106121
rawMultipartFormItems.toList().joinToString(", ") { (k, v) -> "$k=$v" }
107122
)
108123
}
124+
if (imageFiles != null && imageFiles!!.size > Paths.MAX_IMAGES) {
125+
imageFiles?.forEach {
126+
if (!it.delete()) {
127+
return respondFailure(
128+
CouldNotClear,
129+
"Could not remove image from invalid request: $it.\n" +
130+
"Exists? ${if (it.exists()) "true" else "false"}"
131+
)
132+
}
133+
}
134+
return respondFailure(TooManyImages)
135+
}
109136

110137
val path = ServerDatabase.instance.query {
111138
Path.new {
@@ -131,6 +158,7 @@ object NewPathEndpoint : SecureEndpointBase() {
131158
this.description = description
132159
this.builder = builder
133160
this.reBuilder = reBuilder
161+
this.images = imageFiles
134162
this.sector = sector!!
135163
}
136164
}

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/server/endpoints/patch/PatchPathEndpoint.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
88
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Path
99
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Sector
1010
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.info.LastUpdate
11+
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
1112
import com.arnyminerz.escalaralcoiaicomtat.backend.localization.Localization
1213
import com.arnyminerz.escalaralcoiaicomtat.backend.server.endpoints.SecureEndpointBase
1314
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors
15+
import com.arnyminerz.escalaralcoiaicomtat.backend.server.request.save
1416
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondFailure
1517
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondSuccess
18+
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
1619
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.areAllFalse
1720
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.areAllNull
1821
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
@@ -24,6 +27,7 @@ import io.ktor.server.application.ApplicationCall
2427
import io.ktor.server.application.call
2528
import io.ktor.server.util.getValue
2629
import io.ktor.util.pipeline.PipelineContext
30+
import java.io.File
2731
import java.time.Instant
2832

2933
object PatchPathEndpoint : SecureEndpointBase() {
@@ -59,6 +63,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
5963
var description: String? = null
6064
var builder: Builder? = null
6165
var reBuilder: List<Builder>? = null
66+
var imageFiles: List<File>? = null
6267

6368
var sector: Sector? = null
6469

@@ -75,6 +80,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
7580
var removeDescription = false
7681
var removeBuilder = false
7782
var removeReBuilder = false
83+
var removeImages = emptyList<String>()
7884

7985
receiveMultipart(
8086
forEachFormItem = { partData ->
@@ -181,28 +187,58 @@ object PatchPathEndpoint : SecureEndpointBase() {
181187
reBuilder = value.jsonArray.serialize(Builder)
182188
}
183189

190+
"removeImages" -> partData.value.let { value ->
191+
removeImages = value.split('\n').filter { it.isNotBlank() }
192+
}
193+
184194
"path" -> ServerDatabase.instance.query {
185195
sector = Sector.findById(partData.value.toInt())
186196
?: return@query respondFailure(Errors.ParentNotFound)
187197
}
188198
}
199+
},
200+
forEachFileItem = { partData ->
201+
when (partData.name) {
202+
"image" -> imageFiles = (imageFiles ?: emptyList())
203+
.toMutableList()
204+
.apply { add(partData.save(Storage.ImagesDir)) }
205+
}
189206
}
190207
)
191208

192209
if (areAllNull(
193210
displayName, sketchId, height, grade, ending, pitches, stringCount, paraboltCount, burilCount,
194211
pitonCount, spitCount, tensorCount, crackerRequired, friendRequired, lanyardRequired, nailRequired,
195-
pitonRequired, stapesRequired, showDescription, description, builder, reBuilder, sector
212+
pitonRequired, stapesRequired, showDescription, description, builder, reBuilder, imageFiles, sector
196213
) &&
197214
areAllFalse(
198215
removeHeight, removeGrade, removeEnding, removePitches, removeStringCount, removeParaboltCount,
199216
removeBurilCount, removePitonCount, removeSpitCount, removeTensorCount, removeDescription,
200217
removeBuilder, removeReBuilder
201-
)
218+
) &&
219+
removeImages.isEmpty()
202220
) {
203221
return respondSuccess(httpStatusCode = HttpStatusCode.NoContent)
204222
}
205223

224+
if (removeImages.isNotEmpty() && path.images?.isEmpty() == true) {
225+
// There are no images to remove
226+
return respondSuccess(httpStatusCode = HttpStatusCode.NoContent)
227+
}
228+
229+
if (path.images != null && imageFiles != null && path.images!!.size + imageFiles!!.size > Paths.MAX_IMAGES) {
230+
imageFiles?.forEach {
231+
if (!it.delete()) {
232+
return respondFailure(
233+
Errors.CouldNotClear,
234+
"Could not remove image from invalid request: $it.\n" +
235+
"Exists? ${if (it.exists()) "true" else "false"}"
236+
)
237+
}
238+
}
239+
return respondFailure(Errors.TooManyImages)
240+
}
241+
206242
ServerDatabase.instance.query {
207243
displayName?.let { path.displayName = it }
208244
sketchId?.let { path.sketchId = it }
@@ -226,6 +262,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
226262
description?.let { path.description = it }
227263
builder?.let { path.builder = it }
228264
reBuilder?.let { path.reBuilder = it }
265+
imageFiles?.let { path.images = it }
229266
sector?.let { path.sector = it }
230267

231268
if (removeHeight) path.height = null
@@ -242,6 +279,10 @@ object PatchPathEndpoint : SecureEndpointBase() {
242279
if (removeBuilder) path.builder = null
243280
if (removeReBuilder) path.reBuilder = null
244281

282+
if (removeImages.isNotEmpty()) {
283+
path.images = path.images?.filter { !removeImages.contains(it.name) }
284+
}
285+
245286
path.timestamp = Instant.now()
246287
}
247288

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/server/error/Errors.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.arnyminerz.escalaralcoiaicomtat.backend.server.error
22

3+
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
34
import io.ktor.http.HttpStatusCode
45

56
/**
@@ -14,6 +15,7 @@ object Errors {
1415

1516
val MissingData = Error(10, "The request misses some required data.", HttpStatusCode.BadRequest)
1617
val InvalidData = Error(11, "The request has some data of the wrong type.", HttpStatusCode.BadRequest)
18+
val TooManyImages = Error(12, "Too many images added. Maximum: ${Paths.MAX_IMAGES}", HttpStatusCode.BadRequest)
1719

1820
val Conflict = Error(20, "Multiple parameters in the request conflict.", HttpStatusCode.Conflict)
1921

@@ -24,4 +26,6 @@ object Errors {
2426

2527
val DatabaseNotEmpty = Error(40, "Database must be empty", HttpStatusCode.PreconditionFailed)
2628
val NotRunning = Error(41, "Not running", HttpStatusCode.PreconditionFailed)
29+
30+
val CouldNotClear = Error(50, "Could not clear an invalid request", HttpStatusCode.InternalServerError)
2731
}

src/main/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/utils/JsonArrayUtils.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,24 @@ fun <T: Any> Iterable<T>.mapJson(block: (T) -> JSONObject): JSONArray {
5353
}
5454
return array
5555
}
56+
57+
/**
58+
* Maps each element of the iterable to a JSON primitive using the specified block.
59+
*
60+
* The value should be a [Boolean], [Double], [Integer], [JSONArray], [JSONObject], [Long], or [String], or the
61+
* [JSONObject.NULL] object.
62+
*
63+
* Returns a JSON array containing the mapped elements.
64+
*
65+
* @param block the transformation block that converts an element of the iterable to a JSON-supported object.
66+
*
67+
* @return a JSON array containing the mapped elements
68+
*/
69+
fun <T: Any, I> Iterable<T>.mapJsonPrimitive(block: (T) -> I): JSONArray {
70+
val array = JSONArray()
71+
for (i in 0 until count()) {
72+
val entry = block(elementAt(i))
73+
array.put(entry)
74+
}
75+
return array
76+
}

src/test/kotlin/com/arnyminerz/escalaralcoiaicomtat/backend/server/DataProvider.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ object DataProvider {
253253
sectorId: Int?,
254254
skipDisplayName: Boolean = false,
255255
skipSketchId: Boolean = false,
256+
/**
257+
* Should contain the path in resources of all the images to include.
258+
*/
259+
images: List<String>? = null,
256260
assertion: suspend HttpResponse.() -> Int? = {
257261
var pathId: Int? = null
258262
assertSuccess(HttpStatusCode.Created) { data ->
@@ -265,6 +269,10 @@ object DataProvider {
265269
): Int? {
266270
var pathId: Int?
267271

272+
val imagesData = images?.map { path ->
273+
this::class.java.getResourceAsStream(path)!!.use { it.readBytes() }
274+
}
275+
268276
client.submitFormWithBinaryData(
269277
url = "/path",
270278
formData = formData {
@@ -294,6 +302,13 @@ object DataProvider {
294302
append("description", SamplePath.description)
295303
append("builder", SamplePath.builder.toJson().toString())
296304
append("reBuilder", SamplePath.reBuilder.toJson().toString())
305+
306+
imagesData?.forEachIndexed { index, image ->
307+
append("image", image, Headers.build {
308+
append(HttpHeaders.ContentType, "image/jpeg")
309+
append(HttpHeaders.ContentDisposition, "filename=path-$index.jpg")
310+
})
311+
}
297312
}
298313
) {
299314
header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN")

0 commit comments

Comments
 (0)