aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/pinapelz/Database.java140
-rw-r--r--src/main/java/com/pinapelz/FileSystem.java23
-rw-r--r--src/main/java/com/pinapelz/Main.java3
-rw-r--r--src/main/java/com/pinapelz/Retriever.java8
-rw-r--r--src/main/java/com/pinapelz/frontend/App.kt189
-rw-r--r--src/main/java/com/pinapelz/frontend/MultipartFileManager.kt263
-rw-r--r--src/main/java/com/pinapelz/frontend/WebhookManager.kt165
7 files changed, 773 insertions, 18 deletions
diff --git a/src/main/java/com/pinapelz/Database.java b/src/main/java/com/pinapelz/Database.java
index e43212d..0e8e984 100644
--- a/src/main/java/com/pinapelz/Database.java
+++ b/src/main/java/com/pinapelz/Database.java
@@ -7,6 +7,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.*;
+import java.sql.Types;
import java.util.Properties;
public class Database {
@@ -214,6 +215,43 @@ public class Database {
}
}
+ public ResultSet getUniqueOriginalFilesFromPartials(int directoryId, String search) {
+ StringBuilder sql = new StringBuilder("""
+ SELECT DISTINCT
+ original_filename,
+ mime_type,
+ directory_id,
+ MAX(created_at) as created_at,
+ SUM(part_size) as size,
+ MAX(file_description) as file_description
+ FROM file_partials
+ WHERE directory_id = ?
+ """);
+
+ if (search != null && !search.trim().isEmpty()) {
+ sql.append(" AND LOWER(original_filename) LIKE ?");
+ }
+
+ sql.append("""
+ GROUP BY original_filename, mime_type, directory_id
+ ORDER BY original_filename ASC
+ """);
+
+ try {
+ PreparedStatement ps = conn.prepareStatement(sql.toString());
+ int paramIndex = 1;
+ ps.setInt(paramIndex++, directoryId);
+
+ if (search != null && !search.trim().isEmpty()) {
+ ps.setString(paramIndex++, "%" + search.toLowerCase() + "%");
+ }
+
+ return ps.executeQuery();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to fetch unique original files from partials", e);
+ }
+ }
+
public boolean deleteDirectory(int directoryId) throws SQLException {
String checkSql = """
SELECT COUNT(*) as file_count
@@ -246,4 +284,106 @@ public class Database {
}
}
+ public long recordFilePartial(String channelId, String messageId, int directoryId,
+ String partName, int partNumber, long partSize,
+ String originalFilename, String description, String mimeType) throws SQLException {
+ String sql = """
+ INSERT INTO file_partials (
+ disc_channel_id,
+ disc_message_id,
+ directory_id,
+ part_name,
+ part_number,
+ part_size,
+ original_filename,
+ file_description,
+ mime_type,
+ uploaded_via_webhook
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ RETURNING partial_id
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, channelId);
+ ps.setString(2, messageId);
+ ps.setInt(3, directoryId);
+ ps.setString(4, partName);
+ ps.setInt(5, partNumber);
+ ps.setLong(6, partSize);
+ ps.setString(7, originalFilename);
+ ps.setString(8, description);
+ ps.setString(9, mimeType);
+ ps.setBoolean(10, true);
+
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ return rs.getLong("partial_id");
+ }
+ throw new SQLException("Failed to get partial ID");
+ }
+ }
+ }
+
+ public boolean checkPartialExists(String partName, int directoryId) throws SQLException {
+ String sql = """
+ SELECT COUNT(*) as count
+ FROM file_partials
+ WHERE part_name = ? AND directory_id = ?
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, partName);
+ ps.setInt(2, directoryId);
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt("count") > 0;
+ }
+ return false;
+ }
+ }
+ }
+
+ public boolean deleteFilePartials(String originalFilename, int directoryId) throws SQLException {
+ String sql = """
+ DELETE FROM file_partials
+ WHERE original_filename = ? AND directory_id = ?
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, originalFilename);
+ ps.setInt(2, directoryId);
+ int rowsAffected = ps.executeUpdate();
+ return rowsAffected > 0;
+ }
+ }
+
+ public ResultSet getFilePartialsByOriginalFilename(String originalFilename, int directoryId) {
+ String sql = """
+ SELECT
+ partial_id,
+ disc_channel_id,
+ disc_message_id,
+ part_name,
+ part_number,
+ part_size,
+ original_filename,
+ mime_type,
+ uploaded_via_webhook,
+ created_at
+ FROM file_partials
+ WHERE original_filename = ? AND directory_id = ?
+ ORDER BY part_number ASC
+ """;
+
+ try {
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.setString(1, originalFilename);
+ ps.setInt(2, directoryId);
+ return ps.executeQuery();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to fetch file partials", e);
+ }
+ }
+
}
diff --git a/src/main/java/com/pinapelz/FileSystem.java b/src/main/java/com/pinapelz/FileSystem.java
index 109b9ed..e86cf6e 100644
--- a/src/main/java/com/pinapelz/FileSystem.java
+++ b/src/main/java/com/pinapelz/FileSystem.java
@@ -69,4 +69,27 @@ public class FileSystem {
public boolean deleteDirectory(int directoryId) throws SQLException {
return database.deleteDirectory(directoryId);
}
+
+ public long createFilePartial(String channelId, String messageId, int directoryId,
+ String partName, int partNumber, long partSize,
+ String originalFilename, String description, String mimeType) throws SQLException {
+ return database.recordFilePartial(channelId, messageId, directoryId, partName,
+ partNumber, partSize, originalFilename, description, mimeType);
+ }
+
+ public ResultSet getFilePartialsByOriginalFilename(String originalFilename, int directoryId) {
+ return database.getFilePartialsByOriginalFilename(originalFilename, directoryId);
+ }
+
+ public ResultSet getGroupedPartials(int directoryId, String search) {
+ return database.getUniqueOriginalFilesFromPartials(directoryId, search);
+ }
+
+ public boolean deleteFilePartials(String originalFilename, int directoryId) throws SQLException {
+ return database.deleteFilePartials(originalFilename, directoryId);
+ }
+
+ public boolean checkPartialNameConstraint(String partName, int directoryId) throws SQLException {
+ return database.checkPartialExists(partName, directoryId);
+ }
}
diff --git a/src/main/java/com/pinapelz/Main.java b/src/main/java/com/pinapelz/Main.java
index 40b0feb..f3c2ef1 100644
--- a/src/main/java/com/pinapelz/Main.java
+++ b/src/main/java/com/pinapelz/Main.java
@@ -31,8 +31,9 @@ public class Main
}
public static void main(String[] args) throws Exception{
+ String pathToWebhooks = readSetting("WEBHOOKS_TXT");
JDA jda = startBot();
- startFrontend(new Retriever(jda), fileSystem);
+ startFrontend(new Retriever(jda), fileSystem, pathToWebhooks);
}
diff --git a/src/main/java/com/pinapelz/Retriever.java b/src/main/java/com/pinapelz/Retriever.java
index 856dab1..db314fe 100644
--- a/src/main/java/com/pinapelz/Retriever.java
+++ b/src/main/java/com/pinapelz/Retriever.java
@@ -13,6 +13,10 @@ public class Retriever {
}
public String getFileUrl(String channelId, String messageId, String fileName) {
+ return getFileUrl(channelId, messageId, fileName, false);
+ }
+
+ public String getFileUrl(String channelId, String messageId, String fileName, boolean isWebhookUpload) {
TextChannel channel = jda.getTextChannelById(channelId);
if (channel == null) {
throw new RuntimeException("Channel not found or deleted");
@@ -23,10 +27,10 @@ public class Retriever {
for (Message.Attachment file : message.getAttachments()) {
if (file.getFileName().equals(fileName)) {
- return file.getProxyUrl();
+ return isWebhookUpload ? file.getUrl() : file.getProxyUrl();
}
}
throw new RuntimeException("Matching attachment not found");
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/com/pinapelz/frontend/App.kt b/src/main/java/com/pinapelz/frontend/App.kt
index 22806bd..60a4bea 100644
--- a/src/main/java/com/pinapelz/frontend/App.kt
+++ b/src/main/java/com/pinapelz/frontend/App.kt
@@ -5,8 +5,22 @@ import com.pinapelz.Retriever
import com.pinapelz.FileSystem
import java.sql.ResultSet
import java.text.SimpleDateFormat
+import java.io.File
+import java.net.URLEncoder
-fun startFrontend(retriever: Retriever, fileSystem: FileSystem) {
+fun startFrontend(retriever: Retriever, fileSystem: FileSystem, webhooksFile: String) {
+ // Initialize WebhookManager if webhooks file exists
+ val webhookManager = if (File(webhooksFile).exists()) {
+ try {
+ WebhookManager(webhooksFile)
+ } catch (e: Exception) {
+ println("Warning: Failed to initialize webhook manager: ${e.message}")
+ null
+ }
+ } else {
+ println("Warning: Webhooks file not found: $webhooksFile")
+ null
+ }
val app = Javalin.create{};
app.get("/") { ctx ->
@@ -18,6 +32,35 @@ fun startFrontend(retriever: Retriever, fileSystem: FileSystem) {
ctx.html(generateFileSplitterHtml())
}
+ /*
+ {
+ "success": true,
+ "parts": [
+ {
+ "id": "unique-part-id-1",
+ "name": "filename.part001.nitro",
+ "size": 26214400
+ },
+ {
+ "id": "unique-part-id-2",
+ "name": "filename.part002.nitro",
+ "size": 26214400
+ },
+ {
+ "id": "unique-part-id-3",
+ "name": "filename.part003.nitro",
+ "size": 15728640
+ }
+ ]
+}
+
+ */
+ app.post("/api/split") { ctx ->
+ val manager = MultipartFileManager(fileSystem, webhookManager)
+ val result = manager.handleSplitRequest(ctx)
+ ctx.json(result)
+ }
+
app.get("/api/directories") { ctx ->
val directories = mutableListOf<Map<String, Any>>()
val rs = fileSystem.getAllDirectories()
@@ -76,6 +119,21 @@ fun startFrontend(retriever: Retriever, fileSystem: FileSystem) {
}
rs.close()
+ val partialsRs = fileSystem.getGroupedPartials(directoryId, search)
+ while (partialsRs.next()) {
+ val originalName = partialsRs.getString("original_filename")
+ val partialDescription = partialsRs.getString("file_description")
+ files.add(mapOf(
+ "id" to "partial:$originalName|$directoryId",
+ "name" to originalName,
+ "description" to (partialDescription ?: ""),
+ "size" to formatFileSize(partialsRs.getLong("size")),
+ "mimeType" to (partialsRs.getString("mime_type") ?: "application/octet-stream"),
+ "created" to dateFormat.format(partialsRs.getTimestamp("created_at"))
+ ))
+ }
+ partialsRs.close()
+
val html = generateFileTableHtml(files, search, mimeTypeFilter)
ctx.html(html)
ctx.header("HX-Trigger", "updateFileCount")
@@ -138,17 +196,25 @@ fun startFrontend(retriever: Retriever, fileSystem: FileSystem) {
}
app.delete("/api/files/{id}") { ctx ->
- val fileId = ctx.pathParam("id").toIntOrNull()
- if (fileId == null) {
- ctx.status(400).json(mapOf(
- "success" to false,
- "message" to "Invalid file ID"
- ))
- return@delete
- }
+ val idStr = ctx.pathParam("id")
try {
- val deleted = fileSystem.deleteFile(fileId)
+ val deleted = if (idStr.startsWith("partial:")) {
+ val data = idStr.substring("partial:".length).split("|")
+ val filename = data[0]
+ val dirId = data[1].toInt()
+ fileSystem.deleteFilePartials(filename, dirId)
+ } else {
+ val fileId = idStr.toIntOrNull()
+ if (fileId == null) {
+ ctx.status(400).json(mapOf(
+ "success" to false,
+ "message" to "Invalid file ID"
+ ))
+ return@delete
+ }
+ fileSystem.deleteFile(fileId)
+ }
if (deleted) {
ctx.json(mapOf(
"success" to true,
@@ -199,12 +265,105 @@ fun startFrontend(retriever: Retriever, fileSystem: FileSystem) {
}
}
+ app.get("/api/reassemble") { ctx ->
+ val filename = ctx.queryParam("filename") ?: throw io.javalin.http.BadRequestResponse("filename required")
+ val dirId = ctx.queryParam("dir")?.toIntOrNull() ?: throw io.javalin.http.BadRequestResponse("dir id required")
+
+ val rs = fileSystem.getFilePartialsByOriginalFilename(filename, dirId)
+ data class PartInfo(val channelId: String, val messageId: String, val partName: String, val isWebhook: Boolean)
+ val parts = mutableListOf<PartInfo>()
+ var mimeType = "application/octet-stream"
+
+ while (rs.next()) {
+ parts.add(PartInfo(
+ rs.getString("disc_channel_id"),
+ rs.getString("disc_message_id"),
+ rs.getString("part_name"),
+ rs.getBoolean("uploaded_via_webhook")
+ ))
+ mimeType = rs.getString("mime_type") ?: mimeType
+ }
+ rs.close()
+
+ if (parts.isEmpty()) {
+ ctx.status(404).result("No parts found for $filename")
+ return@get
+ }
+
+ ctx.header("Content-Disposition", "attachment; filename=\"$filename\"")
+ ctx.contentType(mimeType)
+
+ ctx.async {
+ try {
+ val outputStream = ctx.res().outputStream
+ for ((index, part) in parts.withIndex()) {
+ var success = false
+ var lastError: Exception? = null
+ for (attempt in 1..3) {
+ try {
+ val url = retriever.getFileUrl(part.channelId, part.messageId, part.partName, part.isWebhook)
+ println("Fetching part ${index + 1}/${parts.size} from: $url (attempt $attempt)")
+
+ val connection = java.net.URL(url).openConnection() as java.net.HttpURLConnection
+ connection.requestMethod = "GET"
+ connection.setRequestProperty("User-Agent", "Mozilla/5.0")
+ connection.connectTimeout = 30000
+ connection.readTimeout = 30000
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) {
+ connection.inputStream.use { input ->
+ input.copyTo(outputStream)
+ }
+ println("Successfully fetched part ${index + 1}/${parts.size}")
+ success = true
+ break
+ } else {
+ println("HTTP $responseCode for part ${index + 1} on attempt $attempt")
+ lastError = Exception("HTTP $responseCode: ${connection.responseMessage}")
+ if (attempt < 3) Thread.sleep(1000 * attempt.toLong())
+ }
+ } catch (e: Exception) {
+ println("Error fetching part ${index + 1} on attempt $attempt: ${e.message}")
+ lastError = e
+ if (attempt < 3) Thread.sleep(1000 * attempt.toLong())
+ }
+ }
+
+ if (!success) {
+ ctx.status(500)
+ ctx.result("Error: Failed to retrieve part ${index + 1} after 3 attempts. ${lastError?.message}")
+ return@async
+ }
+ }
+ outputStream.flush()
+ } catch (e: Exception) {
+ println("Error during file reassembly: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+ }
+
app.get("/fetch") { ctx ->
- val fileId = ctx.queryParam("fileId")
- val fileMetadata = fileSystem.getFileById(Integer.parseInt(fileId));
- print("Retrieving: " + fileMetadata.fileName)
- ctx.redirect(retriever.getFileUrl(fileMetadata.channelId.toString(),
- fileMetadata.messageId.toString(), fileMetadata.fileName));
+ val fileIdStr = ctx.queryParam("fileId") ?: ""
+ if (fileIdStr.startsWith("partial:")) {
+ val data = fileIdStr.substring("partial:".length).split("|")
+ val filename = data[0]
+ val dirId = data[1]
+ ctx.redirect("/api/reassemble?filename=${URLEncoder.encode(filename, "UTF-8")}&dir=$dirId")
+ return@get
+ }
+
+ try {
+ val fileMetadata = fileSystem.getFileById(Integer.parseInt(fileIdStr));
+ println("Retrieving: " + fileMetadata.fileName)
+ val fileUrl = retriever.getFileUrl(fileMetadata.channelId.toString(),
+ fileMetadata.messageId.toString(), fileMetadata.fileName)
+ ctx.redirect(fileUrl)
+ } catch (e: Exception) {
+ println("Failed to retrieve file: ${e.message}")
+ ctx.status(404).result("Error: File not found or has been deleted from Discord. ${e.message}")
+ }
}
app.start(7070)
}
diff --git a/src/main/java/com/pinapelz/frontend/MultipartFileManager.kt b/src/main/java/com/pinapelz/frontend/MultipartFileManager.kt
new file mode 100644
index 0000000..257521d
--- /dev/null
+++ b/src/main/java/com/pinapelz/frontend/MultipartFileManager.kt
@@ -0,0 +1,263 @@
+package com.pinapelz.frontend
+
+import io.javalin.http.Context
+import io.javalin.http.UploadedFile
+import com.pinapelz.FileSystem
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.UUID
+import kotlin.math.ceil
+import kotlin.math.min
+
+
+sealed class SplitConfig {
+ data class BySize(val sizeInBytes: Long) : SplitConfig()
+ data class ByParts(val numParts: Int) : SplitConfig()
+}
+
+data class FilePartMeta(
+ val id: String,
+ val name: String,
+ val size: Long,
+ val path: Path
+)
+
+data class SplitMetadata(
+ val originalFilename: String,
+ val totalSize: Long,
+ val partCount: Int,
+ val parts: List<FilePartMeta>
+)
+
+data class SplitFileResult(
+ val directory: Path,
+ val metadata: SplitMetadata,
+ val uploadResults: List<WebhookUploadResult>? = null
+)
+
+data class ApiSplitResponse(
+ val success: Boolean,
+ val message: String? = null,
+ val parts: List<ApiPartInfo>? = null
+)
+
+data class ApiPartInfo(
+ val id: String,
+ val name: String,
+ val size: Long,
+ val uploaded: Boolean = false,
+ val channelId: String? = null,
+ val messageId: String? = null
+)
+
+class MultipartFileManager(
+ private val fileSystem: FileSystem? = null,
+ private val webhookManager: WebhookManager? = null
+) {
+ fun handleSplitRequest(ctx: Context): ApiSplitResponse {
+ try {
+ val uploadedFile = ctx.uploadedFile("file")
+ ?: return ApiSplitResponse(false, "No file was uploaded")
+
+ val splitMethod = ctx.formParam("split-method") ?: "size"
+ val useWebhook = ctx.formParam("use-webhook")?.toBoolean() ?: false
+ val directoryId = ctx.formParam("directory-id")?.toIntOrNull() ?: 1
+ var filePrefix = ctx.formParam("file-prefix")?.takeIf { it.isNotBlank() }
+ ?: uploadedFile.filename().substringBeforeLast(".")
+ val fileDescription = ctx.formParam("file-description") ?: ""
+
+ // Replace spaces with underscores in prefix
+ filePrefix = filePrefix.replace(" ", "_")
+
+ // Validate prefix doesn't contain spaces
+ if (filePrefix.contains(" ")) {
+ return ApiSplitResponse(false, "File prefix cannot contain spaces. Spaces have been replaced with underscores.")
+ }
+
+ val splitConfig = when {
+ useWebhook -> {
+ SplitConfig.BySize(10 * 1024 * 1024L) // Discord file limit
+ }
+ splitMethod == "size" -> {
+ val partSize = ctx.formParam("part-size")?.toLongOrNull() ?: 25L
+ val sizeUnit = ctx.formParam("size-unit") ?: "MB"
+ val sizeInBytes = when (sizeUnit) {
+ "KB" -> partSize * 1024
+ "GB" -> partSize * 1024 * 1024 * 1024
+ else -> partSize * 1024 * 1024 // MB default
+ }
+ SplitConfig.BySize(sizeInBytes)
+ }
+ else -> {
+ val numParts = ctx.formParam("num-parts")?.toIntOrNull() ?: 5
+ SplitConfig.ByParts(numParts)
+ }
+ }
+
+ val result = splitFile(uploadedFile, config=splitConfig, prefix=filePrefix)
+
+ if (useWebhook && webhookManager != null && fileSystem != null) {
+ return handleWebhookUpload(result, directoryId, uploadedFile.filename(), fileDescription)
+ } else {
+ val apiParts = result.metadata.parts.map { part ->
+ ApiPartInfo(
+ id = part.id,
+ name = part.name,
+ size = part.size,
+ uploaded = false
+ )
+ }
+ return ApiSplitResponse(true, "File split successfully", apiParts)
+ }
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return ApiSplitResponse(false, "Failed to split file: ${e.message}")
+ }
+ }
+
+ private fun handleWebhookUpload(splitResult: SplitFileResult, directoryId: Int, originalFilename: String, description: String): ApiSplitResponse {
+ if (webhookManager == null || fileSystem == null) {
+ return ApiSplitResponse(false, "Webhook manager or file system not configured")
+ }
+
+ try {
+ for (part in splitResult.metadata.parts) {
+ if (fileSystem.checkPartialNameConstraint(part.name, directoryId)) {
+ return ApiSplitResponse(
+ false,
+ "File part '${part.name}' already exists in this directory. Please use a different prefix or delete the existing parts."
+ )
+ }
+ }
+ } catch (e: Exception) {
+ println("Failed to check for existing parts: ${e.message}")
+ return ApiSplitResponse(false, "Failed to validate file parts: ${e.message}")
+ }
+
+ val uploadResults = mutableListOf<ApiPartInfo>()
+ var uploadedCount = 0
+
+ try {
+ for ((index, part) in splitResult.metadata.parts.withIndex()) {
+ println("Uploading part ${index + 1}/${splitResult.metadata.parts.size}: ${part.name}")
+
+ val uploadResult = webhookManager.uploadFile(part.path)
+
+ if (uploadResult.success && uploadResult.channelId != null && uploadResult.messageId != null) {
+ try {
+ val partialId = fileSystem.createFilePartial(
+ uploadResult.channelId,
+ uploadResult.messageId,
+ directoryId,
+ part.name,
+ index + 1,
+ part.size,
+ originalFilename,
+ description,
+ "application/octet-stream"
+ )
+
+ uploadResults.add(ApiPartInfo(
+ id = part.id,
+ name = part.name,
+ size = part.size,
+ uploaded = true,
+ channelId = uploadResult.channelId,
+ messageId = uploadResult.messageId
+ ))
+ uploadedCount++
+
+ println("Successfully uploaded and recorded part: ${part.name} (partial_id: $partialId)")
+ } catch (e: Exception) {
+ println("Failed to record part in database: ${e.message}")
+ uploadResults.add(ApiPartInfo(
+ id = part.id,
+ name = part.name,
+ size = part.size,
+ uploaded = false
+ ))
+ }
+ } else {
+ println("Failed to upload part ${part.name}: ${uploadResult.error}")
+ uploadResults.add(ApiPartInfo(
+ id = part.id,
+ name = part.name,
+ size = part.size,
+ uploaded = false
+ ))
+ }
+ }
+ try {
+ splitResult.directory.toFile().deleteRecursively()
+ } catch (e: Exception) {
+ println("Failed to clean up temporary files: ${e.message}")
+ }
+
+ val message = if (uploadedCount == splitResult.metadata.parts.size) {
+ "All ${uploadedCount} parts uploaded successfully"
+ } else {
+ "Uploaded ${uploadedCount}/${splitResult.metadata.parts.size} parts successfully"
+ }
+
+ return ApiSplitResponse(
+ success = uploadedCount > 0,
+ message = message,
+ parts = uploadResults
+ )
+
+ } catch (e: Exception) {
+ return ApiSplitResponse(
+ false,
+ "Upload process failed: ${e.message}",
+ uploadResults
+ )
+ }
+ }
+ }
+
+ private fun splitFile( uploadedFile: UploadedFile, config: SplitConfig, prefix: String,
+ workingDir: Path = Files.createTempDirectory("split-${prefix}-") ): SplitFileResult {
+ val fileData = uploadedFile.content().readBytes()
+ val fileSize = fileData.size.toLong()
+ val partsMeta = mutableListOf<FilePartMeta>()
+
+ when (config) {
+ is SplitConfig.BySize -> {
+ val partSize = config.sizeInBytes - (16 * 1024);
+ val numParts = ceil(fileSize.toDouble() / partSize.toDouble()).toInt()
+ println("Splitting file: ${fileSize} bytes into ${numParts} parts of max ${partSize} bytes each")
+
+ for (partIndex in 0 until numParts) {
+ val startIndex = (partIndex * partSize).toInt()
+ val endIndex = min(startIndex + partSize.toInt(), fileData.size)
+ val partBytes = fileData.sliceArray(startIndex until endIndex)
+ println("Created part ${partIndex + 1}: ${partBytes.size} bytes")
+
+ val partId = UUID.randomUUID().toString()
+ val partName = "${prefix}.part${String.format("%03d", partIndex + 1)}.nitro"
+ val partPath = workingDir.resolve(partName)
+ Files.write(partPath, partBytes)
+ partsMeta += FilePartMeta(
+ id = partId,
+ name = partName,
+ size = partBytes.size.toLong(),
+ path = partPath
+ )
+ }
+ }
+
+ is SplitConfig.ByParts -> { // TODO: stubbed not yet implemented
+ }
+ }
+ val metadata = SplitMetadata(
+ originalFilename = uploadedFile.filename(),
+ totalSize = fileSize,
+ partCount = partsMeta.size,
+ parts = partsMeta
+ )
+ return SplitFileResult(
+ directory = workingDir,
+ metadata = metadata
+ )
+ }
diff --git a/src/main/java/com/pinapelz/frontend/WebhookManager.kt b/src/main/java/com/pinapelz/frontend/WebhookManager.kt
new file mode 100644
index 0000000..8b35804
--- /dev/null
+++ b/src/main/java/com/pinapelz/frontend/WebhookManager.kt
@@ -0,0 +1,165 @@
+package com.pinapelz.frontend
+
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.time.Instant
+import java.util.concurrent.TimeUnit
+import kotlin.math.max
+
+data class WebhookUploadResult(
+ val success: Boolean,
+ val channelId: String? = null,
+ val messageId: String? = null,
+ val error: String? = null
+)
+
+class WebhookManager(webhooksFilePath: String) {
+ private val webhooks: List<String>
+ private val webhookCooldowns = mutableMapOf<String, Long>()
+ private var currentWebhookIndex = 0
+ private val cooldownPeriodMs = 1000L
+ private val gson = Gson()
+
+ private val client = OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(60, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .build()
+
+ init {
+ val webhooksFile = File(webhooksFilePath)
+ if (!webhooksFile.exists()) {
+ throw IllegalArgumentException("Webhooks file not found: $webhooksFilePath")
+ }
+
+ webhooks = webhooksFile.readLines()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() && !it.startsWith("#") }
+
+ if (webhooks.isEmpty()) {
+ throw IllegalArgumentException("No valid webhooks found in file: $webhooksFilePath")
+ }
+
+ println("Loaded ${webhooks.size} webhooks from $webhooksFilePath")
+ }
+
+ private fun getNextAvailableWebhook(): String? {
+ val currentTime = System.currentTimeMillis()
+ val startIndex = currentWebhookIndex
+
+ do {
+ val webhook = webhooks[currentWebhookIndex]
+ val lastUsed = webhookCooldowns[webhook] ?: 0
+ val timeSinceLastUse = currentTime - lastUsed
+
+ if (timeSinceLastUse >= cooldownPeriodMs) {
+ return webhook
+ }
+
+ currentWebhookIndex = (currentWebhookIndex + 1) % webhooks.size
+ } while (currentWebhookIndex != startIndex)
+ val nextAvailableTime = webhookCooldowns.values.minOrNull() ?: 0
+ val waitTime = max(0, (nextAvailableTime + cooldownPeriodMs) - currentTime)
+
+ if (waitTime > 0) {
+ Thread.sleep(waitTime)
+ return getNextAvailableWebhook()
+ }
+
+ return null
+ }
+
+ fun uploadFile(filePath: Path): WebhookUploadResult {
+ val webhook = getNextAvailableWebhook()
+ ?: return WebhookUploadResult(false, error = "No available webhooks")
+
+ val file = filePath.toFile()
+ if (!file.exists()) {
+ return WebhookUploadResult(false, error = "File does not exist: $filePath")
+ }
+
+ try {
+ val mimeType = Files.probeContentType(filePath) ?: "application/octet-stream"
+ val requestBody = MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart(
+ "file",
+ file.name,
+ file.asRequestBody(mimeType.toMediaTypeOrNull())
+ )
+ .build()
+
+ val request = Request.Builder()
+ .url(webhook)
+ .post(requestBody)
+ .build()
+
+ webhookCooldowns[webhook] = System.currentTimeMillis()
+ currentWebhookIndex = (currentWebhookIndex + 1) % webhooks.size
+
+ client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) {
+ return WebhookUploadResult(
+ false,
+ error = "HTTP ${response.code}: ${response.message}"
+ )
+ }
+
+ val responseBody = response.body?.string()
+ if (responseBody.isNullOrEmpty()) {
+ return WebhookUploadResult(false, error = "Empty response from Discord")
+ }
+
+ try {
+ val jsonObject = gson.fromJson(responseBody, JsonObject::class.java)
+ val channelId = jsonObject.get("channel_id")?.asString
+ val messageId = jsonObject.get("id")?.asString
+
+ println("Discord webhook response - Channel ID: $channelId, Message ID: $messageId")
+
+ if (channelId != null && messageId != null) {
+ return WebhookUploadResult(
+ success = true,
+ channelId = channelId,
+ messageId = messageId
+ )
+ } else {
+ println("Failed to extract IDs from response: $responseBody")
+ return WebhookUploadResult(
+ false,
+ error = "Could not extract channel/message IDs from response"
+ )
+ }
+ } catch (e: Exception) {
+ println("Failed to parse JSON response: ${e.message}")
+ println("Response was: $responseBody")
+ return WebhookUploadResult(
+ false,
+ error = "Failed to parse Discord response: ${e.message}"
+ )
+ }
+ }
+ } catch (e: IOException) {
+ return WebhookUploadResult(false, error = "Network error: ${e.message}")
+ } catch (e: Exception) {
+ return WebhookUploadResult(false, error = "Unexpected error: ${e.message}")
+ }
+ }
+
+ fun getWebhookCount(): Int = webhooks.size
+
+ fun getAvailableWebhookCount(): Long {
+ val currentTime = System.currentTimeMillis()
+ return webhooks.count { webhook ->
+ val lastUsed = webhookCooldowns[webhook] ?: 0
+ (currentTime - lastUsed) >= cooldownPeriodMs
+ }.toLong()
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage