Uploading and tag searching now working

This commit is contained in:
neviyn 2021-04-30 16:25:29 +01:00
parent 1a1594bcc1
commit 52bf9b1898
9 changed files with 117 additions and 39 deletions

2
.gitignore vendored
View File

@ -31,3 +31,5 @@ build/
### VS Code ###
.vscode/
images/

View File

@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.crypto.codec.Hex
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@ -12,14 +11,15 @@ import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.multipart.MultipartFile
import java.io.File
import java.security.MessageDigest
import java.util.*
import javax.validation.constraints.NotEmpty
@Controller
class BaseController
@Autowired constructor(
val imageRepository: ImageRepository
){
) {
@GetMapping("/")
fun landingPage(model: Model): String {
@ -46,24 +46,33 @@ class ImageController
@GetMapping
fun getGalleryPage(
@RequestParam(defaultValue = "1") pageNumber: Int,
@RequestParam(name = "page", defaultValue = "1") pageNumber: Int,
@RequestParam tags: String?,
model: Model
): String {
val page = PageRequest.of(pageNumber - 1, 20)
val images : Page<Image>? = when (tags) {
val images: Page<Image> = when (tags) {
null -> imageRepository.findAll(page) // No tags were given to search for
"" -> imageRepository.findAll(page)
else -> {
val tagData = tags.split(" ") // Tags arrive separated by spaces, tags themselves cannot contain spaces
.distinct() // Eliminate duplicates
.mapNotNull { tagRepository.findByTagIs(it) } // Try to get actual tag objects
val distinctTags = tags.split(" ").distinct()
val tagData = distinctTags.mapNotNull { tagRepository.findByTagIs(it) } // Try to get actual tag objects
when {
tagData.isEmpty() -> null // No tags existed with the specified search terms
else -> imageRepository.findByTags(tagData, page)
tagData.size != distinctTags.size -> null // Error if an invalid tag was supplied
tagData.size == 1 -> {
// Simpler query for single tag searches
val result = imageRepository.findAllByTagsContaining(tagData[0], page)
if (result.isEmpty) null else result
}
else -> {
val result = imageRepository.findByTags(tagData, tagData.size.toLong(), page)
if (result.isEmpty) null else result
}
}
}
}
model.addAttribute("images", images)
} ?: return "noresults"
model.addAttribute("imagePage", images)
return "gallery"
}
@ -76,11 +85,10 @@ class UploadController
val imageRepository: ImageRepository,
val tagRepository: TagRepository,
val userRepository: UserRepository,
val imageConfigurationProperties: ImageConfigurationProperties
val imageConfigurationProperties: ImageConfigurationProperties,
val storage: FileSystemStorage
) {
val digest: MessageDigest = MessageDigest.getInstance("SHA-512/256")
@GetMapping
fun showUploadPage(): String = "upload"
@ -88,17 +96,27 @@ class UploadController
fun uploadFile(
@AuthenticationPrincipal userDetails: CustomUserDetails,
@RequestParam file: MultipartFile,
@RequestParam tags: List<String>
@RequestParam @NotEmpty tags: String
): String {
if (file.isEmpty) return "upload" // TODO: Show error on page, can't upload nothing
val extension = File(file.originalFilename!!).extension
val extension = java.io.File(file.originalFilename!!).extension
if (!imageConfigurationProperties.types.contains(extension)) return "upload" // TODO: Show error on page, unrecognised file type
val user = userRepository.findByName(userDetails.username)!!
val hash: String = Hex.encode(digest.digest(file.bytes)).toString()
val tagData = tags.map { tagRepository.findByTagIs(it) ?: tagRepository.save(Tag(it)) }.toMutableSet()
val outputFile = File(imageConfigurationProperties.directory, "$hash.$extension")
outputFile.writeBytes(file.bytes)
val hash: String = generateFileHash(file)
val tagData = tags.split(" ").map { tagRepository.findByTagIs(it) ?: tagRepository.save(Tag(it)) }.toMutableSet()
storage.addImageFile(hash, extension, file.bytes)
imageRepository.save(Image("$hash.$extension", user, tagData))
return "upload" // TODO: Show success on page
}
}
}
val digest: MessageDigest = MessageDigest.getInstance("SHA-512/256")
/**
* Generate a URL safe string representing the hash of a file
*/
fun generateFileHash(file: MultipartFile): String = Base64
.getUrlEncoder()
.encodeToString(
digest.digest(file.bytes)
)

View File

@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.JoinTable
@ -28,14 +30,14 @@ open class User(
inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")]
)
open var roles: MutableSet<Role> = mutableSetOf(),
@Id open var id: Long = -1
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
)
@Entity
open class Role(
@Column(unique = true)
open var name: String = "",
@Id open var id: Long = -1
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
)
@Entity
@ -52,7 +54,7 @@ open class Image(
inverseJoinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")]
)
open var tags: MutableSet<Tag> = mutableSetOf(),
@Id open var id: Long = -1
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
)
@Entity
@ -60,5 +62,5 @@ open class Tag(
@Column(unique = true)
@Pattern(regexp = "[a-zA-Z0-9_]*") // Only allow alphanumeric and underscores
open var tag: String = "",
@Id open var id: Long = -1
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
)

View File

@ -0,0 +1,27 @@
package uk.co.neviyn.booru
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.io.File
interface FileSystemStorage {
/**
* Add a file to the filesystem with [name] and file [extension]
*/
fun addImageFile(name: String, extension: String, data: ByteArray)
}
@Service
class FileSystemStorageService
@Autowired constructor(
val imageConfigurationProperties: ImageConfigurationProperties
) : FileSystemStorage {
override fun addImageFile(name: String, extension: String, data: ByteArray) {
val outputFile = File(imageConfigurationProperties.directory, "$name.$extension")
outputFile.writeBytes(data)
}
}

View File

@ -7,16 +7,22 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
interface UserRepository : CrudRepository<User, Long> {
fun findByName(name: String) : User?
fun findByName(name: String): User?
}
interface RoleRepository : CrudRepository<Role, Long>
interface ImageRepository : JpaRepository<Image, Long> {
@Query("select i from Image i where ?1 in (i.tags)")
fun findByTags(tags: List<Tag>, pageable: Pageable) : Page<Image>
//@Query("select i from Image i join i.tags t where t in ?1 group by i.id having count(i.id) = ?2")
@Query(
value = "select * from booru.image i where i.id in (select image_id from booru.tag_image ti where ti.tag_id in ?1 group by ti.image_id having count(ti.image_id) = ?2)",
nativeQuery = true
)
fun findByTags(tagIDs: List<Tag>, tagCount: Long, pageable: Pageable): Page<Image>
fun findAllByTagsContaining(tag: Tag, pageable: Pageable): Page<Image>
}
interface TagRepository : CrudRepository<Tag, Long> {
fun findByTagIs(tag: String) : Tag?
fun findByTagIs(tag: String): Tag?
}

View File

@ -5,6 +5,14 @@
<title>Gallery</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-2" th:each="image : ${imagePage.content}">
<!--/*@thymesVar id="image" type="uk.co.neviyn.booru.Image"*/-->
<img class="img-thumbnail" th:src="'/i/' + ${image.filename}" alt=""/>
</div>
</div>
</div>
</body>
</html>

View File

@ -13,24 +13,21 @@
</div>
<div class="row">
<div class="col">
<p class="lead text-center">
Image board.
<p class="text-center">
<a href="/gallery" class="text-decoration-none">Browse all</a>
</p>
</div>
</div>
<div class="row justify-content-center">
<form class="col-6 text-center" action="/gallery">
<label for="imageSearch"></label>
<input class="form-control" id="imageSearch" pattern="[a-zA-Z0-9\s]*" placeholder="Ex: blue_eyes smile" type="search" name="tags">
<div class="d-grid col-4 mx-auto">
<button class="btn btn-primary mt-1" type="submit">Search</button>
</div>
<button class="btn btn-primary mt-1 w-25" type="submit">Search</button>
</form>
</div>
<div class="row mt-3">
<div class="col">
<p class="text-center">
Serving <span th:text="${count}"></span> images.
<p class="text-center lead">
<small class="text-muted">Serving </small><span th:text="${count}"></span> <small class="text-muted">images.</small>
</p>
</div>
</div>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta th:replace="fragments :: header"/>
<title>No Images Found</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col text-center">
<p>Couldn't find any images with those tags.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -12,11 +12,12 @@
</div>
</div>
<div class="row justify-content-center">
<form action="/" class="mb-3 col-6 text-center" enctype="multipart/form-data" method="POST">
<form action="/upload" class="mb-3 col-6 text-center" enctype="multipart/form-data" method="POST">
<input th:name="${_csrf.parameterName}" type="hidden" th:value="${_csrf.token}"/>
<label class="form-label" for="formFile">Image file</label>
<input class="form-control" id="formFile" name="file" type="file" th:accept="${@imageConfigurationProperties.typeListForFormFilter()}">
<label class="form-label" for="formTags">Image tags</label>
<input class="form-control" id="formTags" placeholder="Tags separated by spaces" type="text">
<input class="form-control" id="formTags" placeholder="Tags separated by spaces" type="text" name="tags">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>