Uploading and tag searching now working
This commit is contained in:
parent
1a1594bcc1
commit
52bf9b1898
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,3 +31,5 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
images/
|
@ -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)
|
||||
)
|
@ -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
|
||||
)
|
27
src/main/kotlin/uk/co/neviyn/booru/FileSystemStorage.kt
Normal file
27
src/main/kotlin/uk/co/neviyn/booru/FileSystemStorage.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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?
|
||||
}
|
@ -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>
|
@ -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>
|
||||
|
17
src/main/resources/templates/noresults.html
Normal file
17
src/main/resources/templates/noresults.html
Normal 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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user