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 ### ### VS Code ###
.vscode/ .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.Page
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.crypto.codec.Hex
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
@ -12,8 +11,9 @@ import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.util.*
import javax.validation.constraints.NotEmpty
@Controller @Controller
class BaseController class BaseController
@ -46,24 +46,33 @@ class ImageController
@GetMapping @GetMapping
fun getGalleryPage( fun getGalleryPage(
@RequestParam(defaultValue = "1") pageNumber: Int, @RequestParam(name = "page", defaultValue = "1") pageNumber: Int,
@RequestParam tags: String?, @RequestParam tags: String?,
model: Model model: Model
): String { ): String {
val page = PageRequest.of(pageNumber - 1, 20) 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 null -> imageRepository.findAll(page) // No tags were given to search for
"" -> imageRepository.findAll(page)
else -> { else -> {
val tagData = tags.split(" ") // Tags arrive separated by spaces, tags themselves cannot contain spaces val distinctTags = tags.split(" ").distinct()
.distinct() // Eliminate duplicates val tagData = distinctTags.mapNotNull { tagRepository.findByTagIs(it) } // Try to get actual tag objects
.mapNotNull { tagRepository.findByTagIs(it) } // Try to get actual tag objects
when { when {
tagData.isEmpty() -> null // No tags existed with the specified search terms 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" return "gallery"
} }
@ -76,11 +85,10 @@ class UploadController
val imageRepository: ImageRepository, val imageRepository: ImageRepository,
val tagRepository: TagRepository, val tagRepository: TagRepository,
val userRepository: UserRepository, val userRepository: UserRepository,
val imageConfigurationProperties: ImageConfigurationProperties val imageConfigurationProperties: ImageConfigurationProperties,
val storage: FileSystemStorage
) { ) {
val digest: MessageDigest = MessageDigest.getInstance("SHA-512/256")
@GetMapping @GetMapping
fun showUploadPage(): String = "upload" fun showUploadPage(): String = "upload"
@ -88,17 +96,27 @@ class UploadController
fun uploadFile( fun uploadFile(
@AuthenticationPrincipal userDetails: CustomUserDetails, @AuthenticationPrincipal userDetails: CustomUserDetails,
@RequestParam file: MultipartFile, @RequestParam file: MultipartFile,
@RequestParam tags: List<String> @RequestParam @NotEmpty tags: String
): String { ): String {
if (file.isEmpty) return "upload" // TODO: Show error on page, can't upload nothing 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 if (!imageConfigurationProperties.types.contains(extension)) return "upload" // TODO: Show error on page, unrecognised file type
val user = userRepository.findByName(userDetails.username)!! val user = userRepository.findByName(userDetails.username)!!
val hash: String = Hex.encode(digest.digest(file.bytes)).toString() val hash: String = generateFileHash(file)
val tagData = tags.map { tagRepository.findByTagIs(it) ?: tagRepository.save(Tag(it)) }.toMutableSet() val tagData = tags.split(" ").map { tagRepository.findByTagIs(it) ?: tagRepository.save(Tag(it)) }.toMutableSet()
val outputFile = File(imageConfigurationProperties.directory, "$hash.$extension") storage.addImageFile(hash, extension, file.bytes)
outputFile.writeBytes(file.bytes)
imageRepository.save(Image("$hash.$extension", user, tagData)) imageRepository.save(Image("$hash.$extension", user, tagData))
return "upload" // TODO: Show success on page 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.Column
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.FetchType import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id import javax.persistence.Id
import javax.persistence.JoinColumn import javax.persistence.JoinColumn
import javax.persistence.JoinTable import javax.persistence.JoinTable
@ -28,14 +30,14 @@ open class User(
inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")] inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")]
) )
open var roles: MutableSet<Role> = mutableSetOf(), open var roles: MutableSet<Role> = mutableSetOf(),
@Id open var id: Long = -1 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
) )
@Entity @Entity
open class Role( open class Role(
@Column(unique = true) @Column(unique = true)
open var name: String = "", open var name: String = "",
@Id open var id: Long = -1 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
) )
@Entity @Entity
@ -52,7 +54,7 @@ open class Image(
inverseJoinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")] inverseJoinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")]
) )
open var tags: MutableSet<Tag> = mutableSetOf(), open var tags: MutableSet<Tag> = mutableSetOf(),
@Id open var id: Long = -1 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) open var id: Long = -1
) )
@Entity @Entity
@ -60,5 +62,5 @@ open class Tag(
@Column(unique = true) @Column(unique = true)
@Pattern(regexp = "[a-zA-Z0-9_]*") // Only allow alphanumeric and underscores @Pattern(regexp = "[a-zA-Z0-9_]*") // Only allow alphanumeric and underscores
open var tag: String = "", 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

@ -13,8 +13,14 @@ interface UserRepository : CrudRepository<User, Long> {
interface RoleRepository : CrudRepository<Role, Long> interface RoleRepository : CrudRepository<Role, Long>
interface ImageRepository : JpaRepository<Image, Long> { interface ImageRepository : JpaRepository<Image, Long> {
@Query("select i from Image i where ?1 in (i.tags)") //@Query("select i from Image i join i.tags t where t in ?1 group by i.id having count(i.id) = ?2")
fun findByTags(tags: List<Tag>, pageable: Pageable) : Page<Image> @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> { interface TagRepository : CrudRepository<Tag, Long> {

View File

@ -5,6 +5,14 @@
<title>Gallery</title> <title>Gallery</title>
</head> </head>
<body> <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> </body>
</html> </html>

View File

@ -13,24 +13,21 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<p class="lead text-center"> <p class="text-center">
Image board. <a href="/gallery" class="text-decoration-none">Browse all</a>
</p> </p>
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<form class="col-6 text-center" action="/gallery"> <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"> <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 w-25" type="submit">Search</button>
<button class="btn btn-primary mt-1" type="submit">Search</button>
</div>
</form> </form>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col"> <div class="col">
<p class="text-center"> <p class="text-center lead">
Serving <span th:text="${count}"></span> images. <small class="text-muted">Serving </small><span th:text="${count}"></span> <small class="text-muted">images.</small>
</p> </p>
</div> </div>
</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> </div>
<div class="row justify-content-center"> <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> <label class="form-label" for="formFile">Image file</label>
<input class="form-control" id="formFile" name="file" type="file" th:accept="${@imageConfigurationProperties.typeListForFormFilter()}"> <input class="form-control" id="formFile" name="file" type="file" th:accept="${@imageConfigurationProperties.typeListForFormFilter()}">
<label class="form-label" for="formTags">Image tags</label> <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> <button type="submit" class="btn btn-primary">Submit</button>
</form> </form>
</div> </div>