From 5689cb4267db330766c7fc2c1939d0bc1407bcda Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 13 Aug 2025 21:19:50 +0330 Subject: [PATCH] Work on sending files --- .../Client/ActionHandler.java | 2 +- .../Database/MessageDatabase.java | 87 +++++-- .../Models/FileAttachment.java | 56 ++++- .../Server/ClientHandler.java | 228 ++++++++++++++---- 4 files changed, 295 insertions(+), 78 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 6a8ff7e..6f06582 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1313,7 +1313,7 @@ public class ActionHandler { JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); String senderName = m.optString("sender_name", "Other"); - String content = m.getString("content"); + String content = m.optString("content", ""); String time = m.getString("send_at"); String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 7c8a35b..90ff0fc 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -84,25 +84,54 @@ public class MessageDatabase { public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List attachments) throws SQLException { if (attachments == null || attachments.isEmpty()) return true; - String sql = """ - INSERT INTO message_attachments - (attachment_id, message_id, file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + + final String sql = """ + INSERT INTO message_attachments( + attachment_id, message_id, + file_url, file_type, file_name, file_size, mime_type, + width, height, duration_seconds, thumbnail_url, + media_key, storage_path + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { for (FileAttachment att : attachments) { - ps.setObject(1, UUID.randomUUID()); - ps.setObject(2, messageId); - ps.setString(3, att.getFileUrl()); - ps.setString(4, att.getFileType()); // IMAGE/VIDEO/AUDIO/FILE/GIF/STICKER - ps.setString(5, att.getFileName()); - if (att.getFileSize() != null) ps.setLong(6, att.getFileSize()); else ps.setNull(6, java.sql.Types.BIGINT); - ps.setString(7, att.getMimeType()); - if (att.getWidth() != null) ps.setInt(8, att.getWidth()); else ps.setNull(8, java.sql.Types.INTEGER); - if (att.getHeight() != null) ps.setInt(9, att.getHeight()); else ps.setNull(9, java.sql.Types.INTEGER); - if (att.getDurationSeconds() != null) ps.setInt(10, att.getDurationSeconds()); else ps.setNull(10, java.sql.Types.INTEGER); - ps.setString(11, att.getThumbnailUrl()); + if (att == null) throw new IllegalArgumentException("Attachment is null"); + UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID(); + UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // ساده‌ترین حالت + + String ft = att.getFileType(); + if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) { + throw new IllegalArgumentException("file_type must be IMAGE or AUDIO"); + } + if (att.getStoragePath() == null || att.getStoragePath().isBlank()) { + throw new IllegalArgumentException("storage_path is required for socket downloads"); + } + + int i = 1; + ps.setObject(i++, attachmentId); + ps.setObject(i++, messageId); + //file url (display link) + if (att.getFileUrl() == null || att.getFileUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); + else ps.setString(i++, att.getFileUrl()); + + ps.setString(i++, ft.toUpperCase()); + ps.setString(i++, att.getFileName()); + if (att.getFileSize() == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, att.getFileSize()); + if (att.getMimeType() == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, att.getMimeType()); + if (att.getWidth() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getWidth()); + if (att.getHeight() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getHeight()); + if (att.getDurationSeconds() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getDurationSeconds()); + if (att.getThumbnailUrl() == null || att.getThumbnailUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); + else ps.setString(i++, att.getThumbnailUrl()); + + ps.setObject(i++, mediaKey); + ps.setString(i++, att.getStoragePath()); + ps.addBatch(); + + att.setAttachmentId(attachmentId); + att.setMediaKey(mediaKey); } ps.executeBatch(); return true; @@ -110,15 +139,24 @@ public class MessageDatabase { } - public static boolean saveMessageWithOptionalAttachments(UUID messageId, UUID senderId, UUID receiverId, - String receiverType, String content, String messageType, - List attachments) { + + public static boolean saveMessageWithOptionalAttachments( + UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType, + List attachments + ) { Connection conn = null; try { conn = ConnectionDb.connect(); conn.setAutoCommit(false); - boolean isText = "TEXT".equalsIgnoreCase(messageType); + boolean isText = "TEXT".equalsIgnoreCase(messageType); + boolean isImage = "IMAGE".equalsIgnoreCase(messageType); + boolean isAudio = "AUDIO".equalsIgnoreCase(messageType); + if (!isText && !isImage && !isAudio) { + throw new IllegalArgumentException("messageType must be TEXT, IMAGE, or AUDIO"); + } + if (isText) { if (attachments != null && !attachments.isEmpty()) throw new IllegalArgumentException("TEXT must not have attachments"); @@ -127,9 +165,18 @@ public class MessageDatabase { } else { if (attachments == null || attachments.isEmpty()) throw new IllegalArgumentException("Non-TEXT must have at least one attachment"); + + for (FileAttachment a : attachments) { + if (a == null) throw new IllegalArgumentException("Attachment is null"); + String ft = a.getFileType(); + if (isImage && !"IMAGE".equalsIgnoreCase(ft)) + throw new IllegalArgumentException("All attachments must be IMAGE for messageType=IMAGE"); + if (isAudio && !"AUDIO".equalsIgnoreCase(ft)) + throw new IllegalArgumentException("All attachments must be AUDIO for messageType=AUDIO"); + } } - insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType); + insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType.toUpperCase()); if (!isText) insertAttachmentsTx(conn, messageId, attachments); conn.commit(); diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java index 58f7602..f3f5d9c 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -2,18 +2,22 @@ package org.to.telegramfinalproject.Models; import org.json.JSONObject; import java.util.Objects; +import java.util.UUID; public class FileAttachment { - private final String fileUrl; - private final String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER - private final String fileName; - private final Long fileSize; - private final String mimeType; // e.g., image/png - private final Integer width; - private final Integer height; - private final Integer durationSeconds; // for audio/video - private final String thumbnailUrl; + private UUID attachmentId; // اختیاری؛ اگر null بود، تولید می‌کنیم + private UUID mediaKey; + private String fileUrl; + private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER + private String fileName; + private Long fileSize; + private String mimeType; // e.g., image/png + private Integer width; + private Integer height; + private Integer durationSeconds; // for audio/video + private String thumbnailUrl; + private String storagePath; public FileAttachment(String fileUrl, String fileType, @@ -39,6 +43,10 @@ public class FileAttachment { this(fileUrl, fileType, null, null, null, null, null, null, null); } + public FileAttachment() { + } + + // ساخت از JSON /upload public static FileAttachment fromUploadJson(JSONObject j) { return new FileAttachment( @@ -54,7 +62,6 @@ public class FileAttachment { ); } - // خروجی JSON برای RT/کلاینت public JSONObject toJson() { JSONObject out = new JSONObject() .put("file_url", fileUrl) @@ -123,4 +130,33 @@ public class FileAttachment { ", thumbnailUrl='" + thumbnailUrl + '\'' + '}'; } + + public UUID getAttachmentId() {return attachmentId; + } + + public UUID getMediaKey() {return mediaKey; + } + + public String getStoragePath() {return storagePath; + } + + public void setAttachmentId(UUID attachmentId) {this.attachmentId = attachmentId; + } + + public void setMediaKey(UUID mediaKey) {this.mediaKey = mediaKey; + } + + + public void setFileUrl(String fileUrl) {this.fileUrl = fileUrl; + } + public void setFileType(String fileType){this.fileType = fileType;} + public void setFileName(String fileName){this.fileName = fileName;} + public void setFileSize(Long fileSize){this.fileSize = fileSize;} + public void setMimeType(String mimeType){this.mimeType = mimeType;} + public void setWidth(int width){this.width = width;} + public void setHeight(int height){this.height = height;} + public void setDurationSeconds(Integer durationSeconds){this.durationSeconds = durationSeconds;} + public void setThumbnailUrl(String thumbnailUrl){this.thumbnailUrl = thumbnailUrl;} + public void setStoragePath(String storagePath) {this.storagePath = storagePath; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3ef4a43..76136db 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -11,6 +11,9 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDateTime; import java.util.*; @@ -2609,68 +2612,177 @@ public class ClientHandler implements Runnable { } +// private void handleMediaFrame(DataInputStream dis, PrintWriter out) { +// try { +// // MAGIC = "MDM1" +// final int MAGIC_EXPECTED = 0x4D444D31; +// int magic = dis.readInt(); +// if (magic != MAGIC_EXPECTED) { +// out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); +// out.flush(); +// return; +// } +// +// int headerLen = dis.readInt(); +// if (headerLen <= 0 || headerLen > (64 * 1024)) { +// out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); +// out.flush(); +// return; +// } +// +// byte[] headerBytes = dis.readNBytes(headerLen); +// if (headerBytes.length != headerLen) { +// out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); +// out.flush(); +// return; +// } +// JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8)); +// +// long contentLen = dis.readLong(); +// long MAX_MEDIA = 25L * 1024 * 1024; +// if (contentLen <= 0 || contentLen > MAX_MEDIA) { +// skip(dis, contentLen); +// out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); +// out.flush(); +// return; +// } +// +// UUID messageId = UUID.fromString(h.getString("message_id")); +// UUID senderId = UUID.fromString(h.getString("sender_id")); +// String rType = h.getString("receiver_type"); // private/group/channel +// UUID receiverId = UUID.fromString(h.getString("receiver_id")); +// String messageType = h.getString("message_type"); // IMAGE | AUDIO +// +// if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) { +// skip(dis, contentLen); +// out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); +// out.flush(); +// return; +// } +// +// String fileName = h.optString("file_name", "file.bin"); +// String mimeType = h.optString("mime_type", "application/octet-stream"); +// String text = h.optString("text", ""); +// +// Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null; +// Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null; +// +// if (fileName.length() > 200) fileName = fileName.substring(0, 200); +// +// // مسیر ذخیره +// java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize(); +// java.nio.file.Files.createDirectories(baseDir); +// String kind = "IMAGE".equalsIgnoreCase(messageType) ? "images" : "audios"; +// String subdir = kind + "/" + java.time.LocalDate.now(); +// java.nio.file.Path dir = baseDir.resolve(subdir).normalize(); +// java.nio.file.Files.createDirectories(dir); +// +// String ext = guessExt(fileName, mimeType); +// String storedName = java.util.UUID.randomUUID() + ext; +// java.nio.file.Path target = dir.resolve(storedName).normalize(); +// +// // دریافت بایت‌های فایل +// try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream( +// target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) { +// long remaining = contentLen; +// byte[] buf = new byte[8192]; +// while (remaining > 0) { +// int toRead = (int) Math.min(buf.length, remaining); +// int n = dis.read(buf, 0, toRead); +// if (n == -1) throw new EOFException("stream ended early"); +// fos.write(buf, 0, n); +// remaining -= n; +// } +// } +// +// long fileSize = java.nio.file.Files.size(target); +// String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; +// +// FileAttachment att = new FileAttachment( +// fileUrl, +// messageType.toUpperCase(), // IMAGE/AUDIO +// fileName, +// fileSize, +// mimeType, +// width, +// height, +// null, // durationSeconds +// null // thumbnailUrl +// ); +// +// boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( +// messageId, senderId, receiverId, rType, text, messageType.toUpperCase(), java.util.List.of(att) +// ); +// +// JSONObject ack = new JSONObject() +// .put("status", ok ? "success" : "error") +// .put("message_id", messageId.toString()) +// .put("file_url", fileUrl) +// .put("file_size", fileSize) +// .put("mime_type", mimeType); +// +// out.println(ack.toString()); +// out.flush(); +// +// } catch (Exception e) { +// e.printStackTrace(); +// out.println(new JSONObject().put("status","error").put("message","exception").toString()); +// out.flush(); +// } +// } + + private void handleMediaFrame(DataInputStream dis, PrintWriter out) { try { - // MAGIC = "MDM1" - final int MAGIC_EXPECTED = 0x4D444D31; + final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1" int magic = dis.readInt(); if (magic != MAGIC_EXPECTED) { - out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); - out.flush(); - return; + out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); out.flush(); return; } int headerLen = dis.readInt(); - if (headerLen <= 0 || headerLen > (64 * 1024)) { - out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); - out.flush(); - return; + if (headerLen <= 0 || headerLen > 64 * 1024) { + out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); out.flush(); return; } byte[] headerBytes = dis.readNBytes(headerLen); if (headerBytes.length != headerLen) { - out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); - out.flush(); - return; + out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); out.flush(); return; } + JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8)); long contentLen = dis.readLong(); long MAX_MEDIA = 25L * 1024 * 1024; if (contentLen <= 0 || contentLen > MAX_MEDIA) { skip(dis, contentLen); - out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); - out.flush(); - return; + out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); out.flush(); return; } - // الزامی‌ها - UUID messageId = UUID.fromString(h.getString("message_id")); - UUID senderId = UUID.fromString(h.getString("sender_id")); - String rType = h.getString("receiver_type"); // private/group/channel - UUID receiverId = UUID.fromString(h.getString("receiver_id")); - String messageType = h.getString("message_type"); // IMAGE | AUDIO + UUID messageId = UUID.fromString(h.getString("message_id")); + UUID senderId = UUID.fromString(h.getString("sender_id")); + String rType = h.getString("receiver_type"); // private/group/channel + UUID receiverId = UUID.fromString(h.getString("receiver_id")); + String messageType = h.getString("message_type").toUpperCase(); // IMAGE | AUDIO - if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) { + if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) { skip(dis, contentLen); - out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); - out.flush(); - return; + out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); out.flush(); return; } String fileName = h.optString("file_name", "file.bin"); String mimeType = h.optString("mime_type", "application/octet-stream"); - String text = h.optString("text", ""); + String text = h.optString("text", ""); // کپشن اختیاری - Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null; - Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null; + Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null; + Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null; if (fileName.length() > 200) fileName = fileName.substring(0, 200); - // مسیر ذخیره + // مسیر ذخیره (فیزیکی) java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize(); java.nio.file.Files.createDirectories(baseDir); - String kind = "IMAGE".equalsIgnoreCase(messageType) ? "images" : "audios"; + String kind = "IMAGE".equals(messageType) ? "images" : "audios"; String subdir = kind + "/" + java.time.LocalDate.now(); java.nio.file.Path dir = baseDir.resolve(subdir).normalize(); java.nio.file.Files.createDirectories(dir); @@ -2679,7 +2791,7 @@ public class ClientHandler implements Runnable { String storedName = java.util.UUID.randomUUID() + ext; java.nio.file.Path target = dir.resolve(storedName).normalize(); - // دریافت بایت‌های فایل + // دریافت باینری فایل try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream( target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) { long remaining = contentLen; @@ -2694,30 +2806,52 @@ public class ClientHandler implements Runnable { } long fileSize = java.nio.file.Files.size(target); - String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; - FileAttachment att = new FileAttachment( - fileUrl, - messageType.toUpperCase(), // IMAGE/AUDIO - fileName, - fileSize, - mimeType, - width, - height, - null, // durationSeconds - null // thumbnailUrl - ); + String storagePath = target.toString(); // فقط سرور استفاده کنه + String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; // اختیاری/نمایشی (HTTP لازم نیست) + String mt = messageType; // "IMAGE" یا "AUDIO" + int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0; + int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0; + FileAttachment att = new FileAttachment(); + att.setFileUrl(fileUrl); // اختیاری + att.setFileType(messageType); // IMAGE/AUDIO + att.setFileName(fileName); + att.setFileSize(fileSize); + att.setMimeType(mimeType); + att.setWidth(safeWidth); + att.setHeight(safeHeight); + att.setDurationSeconds(0); + att.setThumbnailUrl(null); + att.setStoragePath(storagePath); // اجباری برای سوکت + // اجازه بده insertAttachmentsTx برایش mediaKey و attachmentId بسازد + java.util.List atts = java.util.List.of(att); boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( - messageId, senderId, receiverId, rType, text, messageType.toUpperCase(), java.util.List.of(att) + messageId, senderId, receiverId, rType, text, messageType, atts ); + UUID mediaKey = null; + try (PreparedStatement q = ConnectionDb.connect().prepareStatement( + "SELECT media_key FROM message_attachments WHERE message_id = ? AND storage_path = ? LIMIT 1" + )) { + q.setObject(1, messageId); + q.setString(2, storagePath); + try (ResultSet rs = q.executeQuery()) { + if (rs.next()) mediaKey = (UUID) rs.getObject(1); + } + } catch (SQLException sqle) { + // در بدترین حالت بدون media_key ACK می‌دیم، ولی بهتره خطا رو لاگ کنیم + sqle.printStackTrace(); + } + JSONObject ack = new JSONObject() .put("status", ok ? "success" : "error") .put("message_id", messageId.toString()) - .put("file_url", fileUrl) + .put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) // برای دانلود سوکتی + .put("file_name", fileName) .put("file_size", fileSize) - .put("mime_type", mimeType); + .put("mime_type", mimeType) + .put("display_path", fileUrl); // صرفاً نمایشی out.println(ack.toString()); out.flush(); @@ -2729,6 +2863,7 @@ public class ClientHandler implements Runnable { } } + private static void skip(DataInputStream dis, long n) throws IOException { if (n <= 0) return; byte[] buf = new byte[8192]; @@ -2997,7 +3132,6 @@ public class ClientHandler implements Runnable { .put("data", chatUpdate); for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload); - // حالا new_message را فقط به غیر از sender List others = new ArrayList<>(allMembers); others.remove(senderId); RealTimeEventDispatcher.broadcastToUsers(others, payload);