Skip to content

Commit

Permalink
Auxiliar images for paths (#28)
Browse files Browse the repository at this point in the history
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
  • Loading branch information
ArnyminerZ authored Sep 29, 2023
1 parent 50fec52 commit a0a1b75
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.Ending
import com.arnyminerz.escalaralcoiaicomtat.backend.data.GradeValue
import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonArray
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonOf
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.mapJsonPrimitive
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialization.JsonSerializable
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialize
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.toJson
import java.io.File
import java.time.Instant
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.exposed.dao.IntEntityClass
Expand Down Expand Up @@ -90,6 +93,10 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
get() = _reBuilder?.jsonArray?.serialize(Builder)
set(value) { _reBuilder = value?.toJson()?.toString() }

var images: List<File>?
get() = _images?.split('\n')?.map { File(Storage.ImagesDir, it) }
set(value) { _images = value?.joinToString("\n") { it.toRelativeString(Storage.ImagesDir) } }

var sector by Sector referencedOn Paths.sector


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

private var _images: String? by Paths.images

override fun toJson(): JSONObject = jsonOf(
"id" to id.value,
"timestamp" to timestamp.toEpochMilli(),
Expand Down Expand Up @@ -141,6 +150,8 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
"builder" to builder,
"re_builder" to reBuilder,

"images" to images?.mapJsonPrimitive { it.toRelativeString(Storage.ImagesDir) },

"sector_id" to sector.id.value
)

Expand Down Expand Up @@ -178,6 +189,7 @@ class Path(id: EntityID<Int>): BaseEntity(id), JsonSerializable {
result = 31 * result + (description?.hashCode() ?: 0)
result = 31 * result + (builder?.hashCode() ?: 0)
result = 31 * result + (reBuilder?.hashCode() ?: 0)
result = 31 * result + (_images?.hashCode() ?: 0)
result = 31 * result + sector.hashCode()
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ package com.arnyminerz.escalaralcoiaicomtat.backend.database.table
import com.arnyminerz.escalaralcoiaicomtat.backend.database.SqlConsts

object Paths: BaseTable() {
/**
* The maximum amount of images allowed in a path.
*/
const val MAX_IMAGES = 10

val displayName = varchar("display_name", SqlConsts.DISPLAY_NAME_LENGTH)
val sketchId = uinteger("sketch_id")

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

val images = varchar("images", SqlConsts.FILE_LENGTH * MAX_IMAGES).nullable().default(null)

val sector = reference("sector", Sectors)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Path
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Sector
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.info.LastUpdate
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
import com.arnyminerz.escalaralcoiaicomtat.backend.localization.Localization
import com.arnyminerz.escalaralcoiaicomtat.backend.server.endpoints.SecureEndpointBase
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.CouldNotClear
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.MissingData
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors.TooManyImages
import com.arnyminerz.escalaralcoiaicomtat.backend.server.request.save
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondFailure
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondSuccess
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonArray
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.jsonOf
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.serialize
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.util.pipeline.PipelineContext
import java.io.File

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

var imageFiles: List<File>? = null

receiveMultipart(
forEachFormItem = { partData ->
when (partData.name) {
Expand Down Expand Up @@ -97,6 +105,13 @@ object NewPathEndpoint : SecureEndpointBase() {
?: return@query respondFailure(Errors.ParentNotFound)
}
}
},
forEachFileItem = { partData ->
when (partData.name) {
"image" -> imageFiles = (imageFiles ?: emptyList())
.toMutableList()
.apply { add(partData.save(Storage.ImagesDir)) }
}
}
)

Expand All @@ -106,6 +121,18 @@ object NewPathEndpoint : SecureEndpointBase() {
rawMultipartFormItems.toList().joinToString(", ") { (k, v) -> "$k=$v" }
)
}
if (imageFiles != null && imageFiles!!.size > Paths.MAX_IMAGES) {
imageFiles?.forEach {
if (!it.delete()) {
return respondFailure(
CouldNotClear,
"Could not remove image from invalid request: $it.\n" +
"Exists? ${if (it.exists()) "true" else "false"}"
)
}
}
return respondFailure(TooManyImages)
}

val path = ServerDatabase.instance.query {
Path.new {
Expand All @@ -131,6 +158,7 @@ object NewPathEndpoint : SecureEndpointBase() {
this.description = description
this.builder = builder
this.reBuilder = reBuilder
this.images = imageFiles
this.sector = sector!!
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import com.arnyminerz.escalaralcoiaicomtat.backend.data.PitchInfo
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Path
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.Sector
import com.arnyminerz.escalaralcoiaicomtat.backend.database.entity.info.LastUpdate
import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
import com.arnyminerz.escalaralcoiaicomtat.backend.localization.Localization
import com.arnyminerz.escalaralcoiaicomtat.backend.server.endpoints.SecureEndpointBase
import com.arnyminerz.escalaralcoiaicomtat.backend.server.error.Errors
import com.arnyminerz.escalaralcoiaicomtat.backend.server.request.save
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondFailure
import com.arnyminerz.escalaralcoiaicomtat.backend.server.response.respondSuccess
import com.arnyminerz.escalaralcoiaicomtat.backend.storage.Storage
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.areAllFalse
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.areAllNull
import com.arnyminerz.escalaralcoiaicomtat.backend.utils.json
Expand All @@ -24,6 +27,7 @@ import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.util.getValue
import io.ktor.util.pipeline.PipelineContext
import java.io.File
import java.time.Instant

object PatchPathEndpoint : SecureEndpointBase() {
Expand Down Expand Up @@ -59,6 +63,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
var description: String? = null
var builder: Builder? = null
var reBuilder: List<Builder>? = null
var imageFiles: List<File>? = null

var sector: Sector? = null

Expand All @@ -75,6 +80,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
var removeDescription = false
var removeBuilder = false
var removeReBuilder = false
var removeImages = emptyList<String>()

receiveMultipart(
forEachFormItem = { partData ->
Expand Down Expand Up @@ -181,28 +187,58 @@ object PatchPathEndpoint : SecureEndpointBase() {
reBuilder = value.jsonArray.serialize(Builder)
}

"removeImages" -> partData.value.let { value ->
removeImages = value.split('\n').filter { it.isNotBlank() }
}

"path" -> ServerDatabase.instance.query {
sector = Sector.findById(partData.value.toInt())
?: return@query respondFailure(Errors.ParentNotFound)
}
}
},
forEachFileItem = { partData ->
when (partData.name) {
"image" -> imageFiles = (imageFiles ?: emptyList())
.toMutableList()
.apply { add(partData.save(Storage.ImagesDir)) }
}
}
)

if (areAllNull(
displayName, sketchId, height, grade, ending, pitches, stringCount, paraboltCount, burilCount,
pitonCount, spitCount, tensorCount, crackerRequired, friendRequired, lanyardRequired, nailRequired,
pitonRequired, stapesRequired, showDescription, description, builder, reBuilder, sector
pitonRequired, stapesRequired, showDescription, description, builder, reBuilder, imageFiles, sector
) &&
areAllFalse(
removeHeight, removeGrade, removeEnding, removePitches, removeStringCount, removeParaboltCount,
removeBurilCount, removePitonCount, removeSpitCount, removeTensorCount, removeDescription,
removeBuilder, removeReBuilder
)
) &&
removeImages.isEmpty()
) {
return respondSuccess(httpStatusCode = HttpStatusCode.NoContent)
}

if (removeImages.isNotEmpty() && path.images?.isEmpty() == true) {
// There are no images to remove
return respondSuccess(httpStatusCode = HttpStatusCode.NoContent)
}

if (path.images != null && imageFiles != null && path.images!!.size + imageFiles!!.size > Paths.MAX_IMAGES) {
imageFiles?.forEach {
if (!it.delete()) {
return respondFailure(
Errors.CouldNotClear,
"Could not remove image from invalid request: $it.\n" +
"Exists? ${if (it.exists()) "true" else "false"}"
)
}
}
return respondFailure(Errors.TooManyImages)
}

ServerDatabase.instance.query {
displayName?.let { path.displayName = it }
sketchId?.let { path.sketchId = it }
Expand All @@ -226,6 +262,7 @@ object PatchPathEndpoint : SecureEndpointBase() {
description?.let { path.description = it }
builder?.let { path.builder = it }
reBuilder?.let { path.reBuilder = it }
imageFiles?.let { path.images = it }
sector?.let { path.sector = it }

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

if (removeImages.isNotEmpty()) {
path.images = path.images?.filter { !removeImages.contains(it.name) }
}

path.timestamp = Instant.now()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.arnyminerz.escalaralcoiaicomtat.backend.server.error

import com.arnyminerz.escalaralcoiaicomtat.backend.database.table.Paths
import io.ktor.http.HttpStatusCode

/**
Expand All @@ -14,6 +15,7 @@ object Errors {

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

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

Expand All @@ -24,4 +26,6 @@ object Errors {

val DatabaseNotEmpty = Error(40, "Database must be empty", HttpStatusCode.PreconditionFailed)
val NotRunning = Error(41, "Not running", HttpStatusCode.PreconditionFailed)

val CouldNotClear = Error(50, "Could not clear an invalid request", HttpStatusCode.InternalServerError)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,24 @@ fun <T: Any> Iterable<T>.mapJson(block: (T) -> JSONObject): JSONArray {
}
return array
}

/**
* Maps each element of the iterable to a JSON primitive using the specified block.
*
* The value should be a [Boolean], [Double], [Integer], [JSONArray], [JSONObject], [Long], or [String], or the
* [JSONObject.NULL] object.
*
* Returns a JSON array containing the mapped elements.
*
* @param block the transformation block that converts an element of the iterable to a JSON-supported object.
*
* @return a JSON array containing the mapped elements
*/
fun <T: Any, I> Iterable<T>.mapJsonPrimitive(block: (T) -> I): JSONArray {
val array = JSONArray()
for (i in 0 until count()) {
val entry = block(elementAt(i))
array.put(entry)
}
return array
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ object DataProvider {
sectorId: Int?,
skipDisplayName: Boolean = false,
skipSketchId: Boolean = false,
/**
* Should contain the path in resources of all the images to include.
*/
images: List<String>? = null,
assertion: suspend HttpResponse.() -> Int? = {
var pathId: Int? = null
assertSuccess(HttpStatusCode.Created) { data ->
Expand All @@ -265,6 +269,10 @@ object DataProvider {
): Int? {
var pathId: Int?

val imagesData = images?.map { path ->
this::class.java.getResourceAsStream(path)!!.use { it.readBytes() }
}

client.submitFormWithBinaryData(
url = "/path",
formData = formData {
Expand Down Expand Up @@ -294,6 +302,13 @@ object DataProvider {
append("description", SamplePath.description)
append("builder", SamplePath.builder.toJson().toString())
append("reBuilder", SamplePath.reBuilder.toJson().toString())

imagesData?.forEachIndexed { index, image ->
append("image", image, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(HttpHeaders.ContentDisposition, "filename=path-$index.jpg")
})
}
}
) {
header(HttpHeaders.Authorization, "Bearer $AUTH_TOKEN")
Expand Down
Loading

0 comments on commit a0a1b75

Please sign in to comment.