first attempt with adding and downloading message attachments

This commit is contained in:
DJ2LS 2024-02-06 20:09:20 +01:00
parent 463a1766f1
commit 8d62550775
9 changed files with 206 additions and 121 deletions

View file

@ -2,13 +2,13 @@
<div class="row justify-content-start mb-2">
<div :class="messageWidthClass">
<div class="card bg-light border-0 text-dark">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
<div v-for="attachment in message.attachments" :key="attachment.id" class="card-header">
<div class="btn-group w-100" role="group">
<button class="btn btn-light text-truncate" disabled>{{ attachment.name }}</button>
<button @click="downloadAttachment(attachment.hash_sha512, attachment.name)" class="btn btn-light w-25"><i class="bi bi-download strong"></i></button>
</div>
</div>
<div class="card-body">
<p class="card-text">{{ message.body }}</p>
@ -33,14 +33,7 @@
<i class="bi bi-info-circle"></i>
</button>
<button
disabled
v-if="getFileContent['filesize'] !== 0"
class="btn btn-outline-secondary border-0 me-1"
@click="downloadAttachment"
>
<i class="bi bi-download"></i>
</button>
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
<i class="bi bi-trash"></i>
@ -61,7 +54,6 @@ import { atob_FD } from "../js/freedata";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { saveAs } from "file-saver";
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
@ -80,40 +72,42 @@ export default {
deleteMessage() {
deleteMessageFromDB(this.message.id);
},
async downloadAttachment() {
async downloadAttachment(hash_sha512, fileName) {
try {
// reset file store
chat.downloadFileFromDB = [];
const jsondata = await getMessageAttachment(hash_sha512);
const byteCharacters = atob(jsondata.data);
const byteArrays = [];
const attachment = await getMessageAttachment(this.message.id);
const blob = new Blob([atob_FD(attachment[2])], {
type: `${attachment[1]};charset=utf-8`,
});
window.focus();
saveAs(blob, attachment[0]);
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, { type: jsondata.type });
const url = URL.createObjectURL(blob);
// Creating a temporary anchor element to download the file
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
// Cleanup
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
} catch (error) {
console.error("Failed to download attachment:", error);
console.error("Failed to download the attachment:", error);
}
},
},
computed: {
getFileContent() {
if (this.message.attachments.length <= 0) {
return { filename: "", filesize: 0, filetype: "" };
}
try {
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
return { filename: filename, filesize: filesize, filetype: filetype };
} catch (e) {
console.log("file not loaded from database - empty?");
// we are only checking against filesize for displaying attachments
return { filesize: 0 };
}
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements

View file

@ -2,14 +2,6 @@
<div class="row justify-content-end mb-2">
<!-- control area -->
<div class="col-auto p-0 m-0">
<button
disabled
v-if="getFileContent['filesize'] !== 0"
class="btn btn-outline-secondary border-0 me-1"
@click="downloadAttachment"
>
<i class="bi bi-download"></i>
</button>
<button
class="btn btn-outline-secondary border-0 me-1"
@ -32,17 +24,17 @@
<i class="bi bi-trash"></i>
</button>
</div>
<!-- message area -->
<div :class="messageWidthClass">
<div class="card bg-secondary text-white">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
<div v-for="attachment in message.attachments" :key="attachment.id" class="card-header">
<div class="btn-group w-100" role="group">
<button class="btn btn-light text-truncate" disabled>{{ attachment.name }}</button>
<button @click="downloadAttachment(attachment.hash_sha512, attachment.name)" class="btn btn-light w-25"><i class="bi bi-download strong"></i></button>
</div>
</div>
<div class="card-body">
<p class="card-text">{{ message.body }}</p>
@ -101,7 +93,6 @@ import {
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { saveAs } from "file-saver";
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
@ -126,42 +117,41 @@ export default {
//console.log(this.infoModal)
//this.infoModal.show()
},
async downloadAttachment() {
async downloadAttachment(hash_sha512, fileName) {
try {
// reset file store
chat.downloadFileFromDB = [];
const jsondata = await getMessageAttachment(hash_sha512);
const byteCharacters = atob(jsondata.data);
const byteArrays = [];
const attachment = await getMessageAttachment(this.message.id);
const blob = new Blob([atob_FD(attachment[2])], {
type: `${attachment[1]};charset=utf-8`,
});
saveAs(blob, attachment[0]);
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, { type: jsondata.type });
const url = URL.createObjectURL(blob);
// Creating a temporary anchor element to download the file
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
// Cleanup
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
} catch (error) {
console.error("Failed to download attachment:", error);
console.error("Failed to download the attachment:", error);
}
},
},
computed: {
getFileContent() {
if (this.message.attachments.length <= 0) {
return { filename: "", filesize: 0, filetype: "" };
}
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
// ensure filesize is 0 for hiding message header if no data is available
if (
typeof filename === "undefined" ||
filename === "" ||
filename === "-"
) {
filesize = 0;
}
return { filename: filename, filesize: filesize, filetype: filetype };
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements

View file

@ -44,34 +44,72 @@ chat.inputText += detail.unicode
const chatModuleMessage=ref(null);
// Function to trigger the hidden file input
function triggerFileInput() {
fileInput.value.click();
}
function transmitNewMessage(){
// Use a ref for storing multiple files
const selectedFiles = ref([]);
const fileInput = ref(null);
function handleFileSelection(event) {
// Reset previously selected files
selectedFiles.value = [];
// if no callsign is selected, assume we are using the first one..
if(typeof(chat.selectedCallsign) == 'undefined'){
// Process each file
for (let file of event.target.files) {
const reader = new FileReader();
reader.onload = () => {
// Convert file content to base64
const base64Content = btoa(reader.result); // Adjust this line if necessary
selectedFiles.value.push({
name: file.name,
size: file.size,
type: file.type,
content: base64Content, // Store base64 encoded content
});
};
reader.readAsBinaryString(file); // Read the file content as binary string
}
}function transmitNewMessage() {
// Check if a callsign is selected, default to the first one if not
if (typeof(chat.selectedCallsign) === 'undefined') {
chat.selectedCallsign = Object.keys(chat.callsign_list)[0];
}
chat.inputText = chat.inputText.trim();
if (chat.inputText.length==0 && chat.inputFileName == "-")
return;
// Proceed only if there is text or files selected
if (chat.inputText.length === 0 && selectedFiles.value.length === 0) return;
const attachments = selectedFiles.value.map(file => {
return {
name: file.name,
type: file.type,
data: file.content
};
});
if (chat.selectedCallsign.startsWith("BC-")) {
return "new broadcast"
// Handle broadcast message differently if needed
return "new broadcast";
} else {
//newMessage(chat.selectedCallsign, chat.inputText, chat.inputFile, chat.inputFileName, chat.inputFileSize, chat.inputFileType)
newMessage(chat.selectedCallsign, chat.inputText)
// If there are attachments, send them along with the message
if (attachments.length > 0) {
newMessage(chat.selectedCallsign, chat.inputText, attachments);
} else {
// Send text only if no attachments are selected
newMessage(chat.selectedCallsign, chat.inputText);
}
}
// finally do a cleanup
//chatModuleMessage.reset();
// Cleanup after sending message
chat.inputText = '';
chatModuleMessage.value="";
// @ts-expect-error
resetFile()
chatModuleMessage.value = "";
selectedFiles.value = []; // Clear selected files after sending
// Reset any other states or UI elements as necessary
}
function resetFile(event){
@ -140,9 +178,9 @@ function calculateTimeNeeded(){
return obj.snr === snrList[i].snr
})
calculatedSpeedPerMinutePER0.push(chat.inputFileSize / result.bpm)
calculatedSpeedPerMinutePER25.push(chat.inputFileSize / (result.bpm * 0.75))
calculatedSpeedPerMinutePER75.push(chat.inputFileSize / (result.bpm * 0.25))
calculatedSpeedPerMinutePER0.push(totalSize / result.bpm)
calculatedSpeedPerMinutePER25.push(totalSize / (result.bpm * 0.75))
calculatedSpeedPerMinutePER75.push(totalSize / (result.bpm * 0.25))
}
@ -202,11 +240,11 @@ const speedChartData = computed(() => ({
<!-- trigger file selection modal -->
<!--
<button type="button" class="btn btn-outline-secondary border-0 rounded-pill me-1" data-bs-toggle="modal" data-bs-target="#fileSelectionModal">
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
</button>
-->
<textarea
@ -264,20 +302,54 @@ const speedChartData = computed(() => ({
</div>
</div>
<!--
<div class="input-group-text mb-3">
<input class="" type="file" ref="doc" @change="readFile" />
</div>
-->
<div class="container w-100 mb-3">
<!-- Button that user will click to open file dialog -->
<button class="btn btn-primary w-100" @click="triggerFileInput">Attach Files</button>
<!-- Hidden file input -->
<input type="file" multiple ref="fileInput" @change="handleFileSelection" style="display: none;" />
</div>
<div class="container-fluid px-0">
<div class="d-flex flex-row overflow-auto bg-light rounded-3 p-2 border border-1">
<div v-for="(file, index) in selectedFiles" :key="index" class="pe-2">
<div class="card" style=" min-width: 10rem; max-width: 10rem;">
<!-- Card Header with Remove Button -->
<div class="card-header d-flex justify-content-between align-items-center">
<span class="text-truncate">{{ file.name }}</span>
<button class="btn btn-close" @click="removeFile(index)"></button>
</div>
<div class="card-body">
<p class="card-text">...</p>
</div>
<div class="card-footer text-muted">
{{ file.type }}
</div>
<div class="card-footer text-muted">
{{ file.size }} bytes
</div>
</div>
</div>
</div>
</div>
<hr>
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
<button type="button" class="btn btn-secondary">Type</button>
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileType}}</button>
</div>
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
<button type="button" class="btn btn-secondary">Size</button>
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileSize}}</button>
<button type="button" class="btn btn-secondary">total size</button>
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileSize}} {{totalSizeFormatted}}</button>
</div>
<Line :data="speedChartData" />

View file

@ -155,10 +155,16 @@ export async function getFreedataMessages() {
processFreedataMessages(res);
}
export async function sendFreedataMessage(destination, body) {
export async function getFreedataAttachmentBySha512(data_sha512) {
let res = await apiGet(`/freedata/messages/attachment/${data_sha512}`);
return res;
}
export async function sendFreedataMessage(destination, body, attachments) {
return await apiPost("/freedata/messages", {
destination: destination,
body: body,
attachments: attachments,
});
}

View file

@ -10,6 +10,7 @@ import {
sendFreedataMessage,
deleteFreedataMessage,
retransmitFreedataMessage,
getFreedataAttachmentBySha512,
} from "./api";
interface Message {
@ -76,8 +77,9 @@ function createSortedMessagesList(data: {
return callsignMessages;
}
export function newMessage(dxcall, body) {
sendFreedataMessage(dxcall, body);
export function newMessage(dxcall, body, attachments) {
console.log(attachments)
sendFreedataMessage(dxcall, body, attachments);
}
/* ------ TEMPORARY DUMMY FUNCTIONS --- */
@ -99,6 +101,6 @@ export function requestMessageInfo(id) {
return;
}
export function getMessageAttachment(id) {
return;
export async function getMessageAttachment(data_sha512) {
return await getFreedataAttachmentBySha512(data_sha512)
}

View file

@ -6,6 +6,7 @@ def validate_freedata_callsign(callsign):
return re.compile(regexp).match(callsign) is not None
def validate_message_attachment(attachment):
print(attachment)
for field in ['name', 'type', 'data']:
if field not in attachment:
raise ValueError(f"Attachment missing '{field}'")

View file

@ -63,4 +63,19 @@ class DatabaseManagerAttachments(DatabaseManager):
def get_attachments_by_message_id_json(self, message_id):
attachments = self.get_attachments_by_message_id(message_id)
return json.dumps(attachments)
return json.dumps(attachments)
def get_attachment_by_sha512(self, hash_sha512):
session = self.get_thread_scoped_session()
try:
attachment = session.query(Attachment).filter_by(hash_sha512=hash_sha512).first()
if attachment:
return attachment.to_dict() # Assuming you have a to_dict method
else:
self.log(f"No attachment found with SHA-512 hash: {hash_sha512}")
return None
except Exception as e:
self.log(f"Error fetching attachment with SHA-512 hash {hash_sha512}: {e}", isWarning=True)
return None
finally:
session.remove()

View file

@ -114,7 +114,7 @@ class Attachment(Base):
return {
'id': self.id,
'name': self.name,
'data_type': self.data_type,
'type': self.data_type,
'data': self.data,
'checksum_crc32': self.checksum_crc32,
'hash_sha512' : self.hash_sha512

View file

@ -274,6 +274,11 @@ def get_message_attachments(message_id):
attachments = DatabaseManagerAttachments(app.event_manager).get_attachments_by_message_id_json(message_id)
return api_response(attachments)
@app.route('/freedata/messages/attachment/<string:data_sha512>', methods=['GET'])
def get_message_attachment(data_sha512):
attachment = DatabaseManagerAttachments(app.event_manager).get_attachment_by_sha512(data_sha512)
return api_response(attachment)
@app.route('/freedata/beacons', methods=['GET'])
def get_all_beacons():
beacons = DatabaseManagerBeacon(app.event_manager).get_all_beacons()