Skip to content

Commit

Permalink
feat: upload files
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Apr 14, 2024
1 parent 0c3d983 commit 3d3407d
Show file tree
Hide file tree
Showing 20 changed files with 297 additions and 5 deletions.
2 changes: 1 addition & 1 deletion groupeminaste-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ kotlin {
implementation("me.nathanfallet.ktorx:ktor-routers-admin:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers-admin-locale:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-sentry:$ktorxVersion")
implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.2.3")
implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.3.0-alpha1")

implementation("com.mysql:mysql-connector-j:8.0.33")
implementation("at.favre.lib:bcrypt:0.9.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package me.nathanfallet.groupeminaste.controllers.files

import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import kotlinx.datetime.Clock
import me.nathanfallet.cloudflare.models.r2.Object
import me.nathanfallet.groupeminaste.usecases.files.IListFilesUseCase
import me.nathanfallet.groupeminaste.usecases.files.IUploadFileUseCase
import me.nathanfallet.ktorx.models.responses.RedirectResponse

class FilesController(
private val listFilesUseCase: IListFilesUseCase,
private val uploadFileUseCase: IUploadFileUseCase,
) : IFilesController {

override suspend fun list(): List<Object> =
emptyList() //listFilesUseCase() // To get fixed in cloudflare-api-client

override suspend fun upload(call: ApplicationCall): RedirectResponse {
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
if (part is PartData.FileItem) {
uploadFileUseCase(
part.originalFileName ?: "file-${Clock.System.now()}",
part.streamProvider(),
part.contentType ?: ContentType.Application.OctetStream
)
}
part.dispose()
}
return RedirectResponse("files")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package me.nathanfallet.groupeminaste.controllers.files

import me.nathanfallet.groupeminaste.controllers.models.AdminUnitRouter
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase

class FilesRouter(
controller: IFilesController,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
) : AdminUnitRouter(
controller,
IFilesController::class,
getLocaleForCallUseCase,
translateUseCase,
getAdminMenuForCallUseCase,
route = "files"
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package me.nathanfallet.groupeminaste.controllers.files

import io.ktor.server.application.*
import me.nathanfallet.cloudflare.models.r2.Object
import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.models.annotations.AdminTemplateMapping
import me.nathanfallet.ktorx.models.annotations.ListModelPath
import me.nathanfallet.ktorx.models.annotations.Path
import me.nathanfallet.ktorx.models.responses.RedirectResponse

interface IFilesController : IUnitController {

@AdminTemplateMapping("admin/files/list.ftl")
@ListModelPath
suspend fun list(): List<Object>

@AdminTemplateMapping("admin/files/list.ftl")
@Path("POST", "/")
suspend fun upload(call: ApplicationCall): RedirectResponse

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package me.nathanfallet.groupeminaste.controllers.web
import me.nathanfallet.groupeminaste.models.application.GetInTouchPayload
import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.models.annotations.Path
import me.nathanfallet.ktorx.models.annotations.PathParameter
import me.nathanfallet.ktorx.models.annotations.Payload
import me.nathanfallet.ktorx.models.annotations.TemplateMapping
import me.nathanfallet.ktorx.models.responses.RedirectResponse
Expand All @@ -21,4 +22,8 @@ interface IWebController : IUnitController {
@Path("POST", "/getintouch")
suspend fun getInTouch(@Payload payload: GetInTouchPayload)

@TemplateMapping("public/home.ftl")
@Path("GET", "/infos/{file}")
suspend fun infos(@PathParameter file: String): RedirectResponse

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ class WebController(
"https://discord.gg/PeTpuCWnqs", true
)

override suspend fun getInTouch(payload: GetInTouchPayload) {
getInTouchUseCase(payload)
}
override suspend fun getInTouch(payload: GetInTouchPayload) = getInTouchUseCase(payload)

override suspend fun infos(file: String) = RedirectResponse(
"https://cdn.groupe-minaste.org/$file", true
)

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package me.nathanfallet.groupeminaste.plugins

import io.ktor.server.application.*
import me.nathanfallet.cloudflare.client.CloudflareClient
import me.nathanfallet.cloudflare.client.ICloudflareClient
import me.nathanfallet.cloudflare.r2.IR2Client
import me.nathanfallet.cloudflare.r2.R2Client
import me.nathanfallet.groupeminaste.controllers.auth.AuthController
import me.nathanfallet.groupeminaste.controllers.auth.AuthRouter
import me.nathanfallet.groupeminaste.controllers.auth.IAuthController
import me.nathanfallet.groupeminaste.controllers.dashboard.DashboardController
import me.nathanfallet.groupeminaste.controllers.dashboard.DashboardRouter
import me.nathanfallet.groupeminaste.controllers.dashboard.IDashboardController
import me.nathanfallet.groupeminaste.controllers.files.FilesController
import me.nathanfallet.groupeminaste.controllers.files.FilesRouter
import me.nathanfallet.groupeminaste.controllers.files.IFilesController
import me.nathanfallet.groupeminaste.controllers.projects.*
import me.nathanfallet.groupeminaste.controllers.web.IWebController
import me.nathanfallet.groupeminaste.controllers.web.WebController
Expand All @@ -31,6 +38,10 @@ import me.nathanfallet.groupeminaste.services.jwt.IJWTService
import me.nathanfallet.groupeminaste.services.jwt.JWTService
import me.nathanfallet.groupeminaste.usecases.application.*
import me.nathanfallet.groupeminaste.usecases.auth.*
import me.nathanfallet.groupeminaste.usecases.files.IListFilesUseCase
import me.nathanfallet.groupeminaste.usecases.files.IUploadFileUseCase
import me.nathanfallet.groupeminaste.usecases.files.ListFilesUseCase
import me.nathanfallet.groupeminaste.usecases.files.UploadFileUseCase
import me.nathanfallet.groupeminaste.usecases.users.GetUserForCallUseCase
import me.nathanfallet.groupeminaste.usecases.web.GetAdminMenuForCallUseCase
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
Expand Down Expand Up @@ -101,6 +112,18 @@ fun Application.configureKoin() {
environment.config.property("discord.getInTouch").getString()
)
}
single<ICloudflareClient> {
CloudflareClient(
environment.config.property("cloudflare.token").getString()
)
}
single<IR2Client> {
R2Client(
environment.config.property("cloudflare.id").getString(),
environment.config.property("cloudflare.secret").getString(),
environment.config.property("cloudflare.account").getString()
)
}
}
val repositoryModule = module {
// Application
Expand Down Expand Up @@ -130,6 +153,21 @@ fun Application.configureKoin() {
single<IClearSessionForCallUseCase> { ClearSessionForCallUseCase() }
single<ILoginUseCase> { LoginUseCase(get(), get()) }

// Files
single<IListFilesUseCase> {
ListFilesUseCase(
get(),
environment.config.property("cloudflare.bucket").getString()
)
}
single<IUploadFileUseCase> {
UploadFileUseCase(
get(),
get(),
environment.config.property("cloudflare.bucket").getString()
)
}

// Web
single<IGetAdminMenuForCallUseCase> { GetAdminMenuForCallUseCase(get(), get(), get()) }

Expand Down Expand Up @@ -185,6 +223,7 @@ fun Application.configureKoin() {
)
}
single<IDashboardController> { DashboardController() }
single<IFilesController> { FilesController(get(), get()) }

// Auth
single<IAuthController> { AuthController(get(), get(), get()) }
Expand Down Expand Up @@ -216,6 +255,7 @@ fun Application.configureKoin() {
single { WebRouter(get(), get()) }
single { AuthRouter(get(), get()) }
single { DashboardRouter(get(), get(), get(), get()) }
single { FilesRouter(get(), get(), get(), get()) }
single { ProjectsRouter(get(), get(), get(), get()) }
single { ProjectLinksRouter(get(), get(), get(), get(), get()) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.servers.Server
import me.nathanfallet.groupeminaste.controllers.auth.AuthRouter
import me.nathanfallet.groupeminaste.controllers.dashboard.DashboardRouter
import me.nathanfallet.groupeminaste.controllers.files.FilesRouter
import me.nathanfallet.groupeminaste.controllers.projects.ProjectLinksRouter
import me.nathanfallet.groupeminaste.controllers.projects.ProjectsRouter
import me.nathanfallet.groupeminaste.controllers.web.WebRouter
Expand All @@ -34,6 +35,7 @@ fun Application.configureRouting() {
get<WebRouter>(),
get<AuthRouter>(),
get<DashboardRouter>(),
get<FilesRouter>(),
get<ProjectsRouter>(),
get<ProjectLinksRouter>(),
OpenAPIRouter(), // OpenAPI should be last
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package me.nathanfallet.groupeminaste.usecases.files

import me.nathanfallet.cloudflare.models.r2.Object
import me.nathanfallet.usecases.base.IUnitSuspendUseCase

interface IListFilesUseCase : IUnitSuspendUseCase<List<Object>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.groupeminaste.usecases.files

import io.ktor.http.*
import me.nathanfallet.cloudflare.models.r2.InputStream
import me.nathanfallet.usecases.base.ITripleSuspendUseCase

interface IUploadFileUseCase : ITripleSuspendUseCase<String, InputStream, ContentType, Unit>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.nathanfallet.groupeminaste.usecases.files

import me.nathanfallet.cloudflare.models.r2.Object
import me.nathanfallet.cloudflare.r2.IR2Client

class ListFilesUseCase(
private val r2Client: IR2Client,
private val bucket: String,
) : IListFilesUseCase {

override suspend fun invoke(): List<Object> = r2Client.listObjectsV2(bucket).contents

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package me.nathanfallet.groupeminaste.usecases.files

import io.ktor.http.*
import me.nathanfallet.cloudflare.client.ICloudflareClient
import me.nathanfallet.cloudflare.models.r2.InputStream
import me.nathanfallet.cloudflare.r2.IR2Client

class UploadFileUseCase(
private val r2Client: IR2Client,
private val cloudflareClient: ICloudflareClient,
private val bucket: String,
) : IUploadFileUseCase {

override suspend fun invoke(input1: String, input2: InputStream, input3: ContentType) {
r2Client.putObject(bucket, input1, input2, input3)
cloudflareClient.zones.purgeCache(
"72356ace5c6fddc79486e6aedab34eb7",
listOf("https://cdn.groupe-minaste.org/${input1.removePrefix("/")}")
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class GetAdminMenuForCallUseCase(
override suspend fun invoke(input: ApplicationCall): List<WebMenu> {
requireUserForCallUseCase(input) as User
val locale = getLocaleForCallUseCase(input)
return listOf("dashboard", "projects")
return listOf("dashboard", "projects", "files")
.map {
WebMenu(
it,
Expand Down
11 changes: 11 additions & 0 deletions groupeminaste-backend/src/commonMain/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ discord {
getInTouch = ""
getInTouch = ${?DISCORD_GET_IN_TOUCH}
}
cloudflare {
account = ""
token = ""
id = ""
secret = ""
bucket = "groupeminaste"
account = ${?CLOUDFLARE_ACCOUNT}
token = ${?CLOUDFLARE_TOKEN}
id = ${?CLOUDFLARE_ID}
secret = ${?CLOUDFLARE_SECRET}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ email {
discord {
getInTouch = ""
}
cloudflare {
account = ""
token = ""
id = ""
secret = ""
bucket = "groupeminaste"
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ admin_links_url=URL
admin_links_view=View links for this project
home_contact_confirmation=Thank you for your message! We will get back to you as soon as possible.
home_contact_back=Go back to home
admin_menu_files=Files
admin_files_key=File URL
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<#import "../template.ftl" as template>
<@template.page>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card card-body">
<form action="/${locale}/admin/files" class="form-control dropzone" id="dropzone">
<div class="fallback">
<input name="file" type="file" multiple/>
</div>
</form>
</div>
<div class="card mt-4">
<div class="table-responsive">
<table class="table table-flush" id="datatable-search">
<thead class="thead-light">
<tr>
<th><@t key="admin_files_key" /></th>
</tr>
</thead>
<tbody>
<#list items as file>
<tr>
<td class="font-weight-bold">
<span class="my-2 text-xs"><a href="https://cdn.groupe-minaste.org/${file.key}">https://cdn.groupe-minaste.org/${file.key}</a></span>
</td>
</tr>
</#list>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@3.0.2/dist/umd/simple-datatables.js"></script>
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<script>
const dataTableSearch = new simpleDatatables.DataTable("#datatable-search", {
searchable: true,
fixedHeight: false,
perPageSelect: false
});
Dropzone.autoDiscover = false;
var drop = document.getElementById('dropzone')
var myDropzone = new Dropzone(drop, {
url: "/${locale}/admin/files",
addRemoveLinks: true
});
</script>
</@template.page>

0 comments on commit 3d3407d

Please sign in to comment.