Working on a better file upload element and component. Slow progress...
This commit is contained in:
@@ -19,6 +19,41 @@ import {
|
||||
let ae_promises: key_val = {}; // Promise<any>;
|
||||
|
||||
|
||||
// Updated 2024-08-12
|
||||
async function check_hosted_file_obj_w_hash(
|
||||
{
|
||||
api_cfg,
|
||||
hosted_file_hash,
|
||||
check_for_local=true,
|
||||
params={},
|
||||
return_meta=false,
|
||||
log_lvl=0
|
||||
} : {
|
||||
api_cfg: any,
|
||||
hosted_file_hash: string,
|
||||
check_for_local?: boolean,
|
||||
params?: key_val,
|
||||
return_meta?: boolean,
|
||||
log_lvl?: number
|
||||
}
|
||||
) {
|
||||
console.log('*** stores_event_api.js: check_hosted_file_obj_w_hash() ***');
|
||||
|
||||
const endpoint = `/hosted_file/hash/${hosted_file_hash}`;
|
||||
if (check_for_local) {
|
||||
params['check_for_local'] = true;
|
||||
}
|
||||
let check_hosted_file_obj_w_hash_get_promise = await api.get_object({
|
||||
api_cfg: api_cfg,
|
||||
endpoint: endpoint,
|
||||
params: params,
|
||||
return_meta: return_meta,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return check_hosted_file_obj_w_hash_get_promise;
|
||||
}
|
||||
|
||||
|
||||
// Updated 2024-03-29
|
||||
async function handle_load_ae_obj_id__site_domain(
|
||||
{
|
||||
@@ -364,6 +399,7 @@ async function handle_download_export__obj_type(
|
||||
|
||||
|
||||
let export_obj = {
|
||||
check_hosted_file_obj_w_hash: check_hosted_file_obj_w_hash,
|
||||
handle_load_ae_obj_id__site_domain: handle_load_ae_obj_id__site_domain,
|
||||
handle_load_ae_obj_code__data_store: handle_load_ae_obj_code__data_store,
|
||||
handle_load_ae_obj_id__person: handle_load_ae_obj_id__person,
|
||||
|
||||
@@ -89,6 +89,10 @@ let events_local_data_struct: key_val = {
|
||||
'classes__form': 'border border-surface-200 p-4 space-y-4 rounded-container-token',
|
||||
},
|
||||
|
||||
// Event Files - uploads for sessions, presenters, etc
|
||||
'files': {
|
||||
},
|
||||
|
||||
// Event Presentation Launcher
|
||||
'launcher': {
|
||||
qry_max__sessions: 75,
|
||||
@@ -241,6 +245,15 @@ let events_session_data_struct: key_val = {
|
||||
qr_scan_result: null,
|
||||
},
|
||||
|
||||
// Event Files - uploads for sessions, presenters, etc
|
||||
'files': {
|
||||
disable_submit__event_file_obj: null,
|
||||
status__submit: null,
|
||||
status__file_list: null, // processing, complete
|
||||
|
||||
processed_file_list: [],
|
||||
},
|
||||
|
||||
// Event Presentation Launcher
|
||||
'launcher': {
|
||||
},
|
||||
|
||||
@@ -33,6 +33,63 @@ function number_w_commas(x) {
|
||||
}
|
||||
|
||||
|
||||
// Updated 2024-08-12
|
||||
function guess_file_name(filename_string: string) {
|
||||
// console.log('*** guess_file_name() ***');
|
||||
if (!filename_string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (filename_string.includes('.')) {
|
||||
let file_name = filename_string.substring(0, filename_string.lastIndexOf('.'));
|
||||
// console.log(file_name);
|
||||
return file_name;
|
||||
} else {
|
||||
return filename_string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Updated 2024-08-12
|
||||
function guess_file_extension(filename_string: string) {
|
||||
// console.log('*** guess_file_extension() ***');
|
||||
if (!filename_string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!filename_string.includes('.')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let file_extension = filename_string.substring(filename_string.lastIndexOf('.') + 1, filename_string.length) || filename_string;
|
||||
// console.log(file_extension);
|
||||
return file_extension;
|
||||
}
|
||||
|
||||
|
||||
// Updated 2024-08-12
|
||||
async function get_file_hash(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let file_reader = new FileReader();
|
||||
|
||||
file_reader.onload = async function() {
|
||||
if (file_reader.result.byteLength !== file.size) {
|
||||
console.log('File was not read completely');
|
||||
reject("Error reading the file");
|
||||
}
|
||||
|
||||
const hash_buffer = await crypto.subtle.digest('SHA-256', file_reader.result);
|
||||
const hash_array = Array.from(new Uint8Array(hash_buffer));
|
||||
const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
resolve(hash_hex);
|
||||
};
|
||||
|
||||
file_reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* This utility function looks for any form data with the prefixed name passed and returns a new object.
|
||||
* This function is used heavily! Be very careful making changes!!!
|
||||
* If rm_empty_id then it will remove/ignore fields matching. This helps with the API and new records/objects
|
||||
@@ -726,6 +783,9 @@ export let ae_util = {
|
||||
iso_datetime_formatter: iso_datetime_formatter,
|
||||
format_bytes: format_bytes,
|
||||
number_w_commas: number_w_commas,
|
||||
guess_file_name: guess_file_name,
|
||||
guess_file_extension: guess_file_extension,
|
||||
get_file_hash: get_file_hash,
|
||||
extract_prefixed_form_data: extract_prefixed_form_data,
|
||||
process_data_string: process_data_string,
|
||||
handle_url_and_message: handle_url_and_message,
|
||||
|
||||
413
src/lib/element_input_file.svelte
Normal file
413
src/lib/element_input_file.svelte
Normal file
@@ -0,0 +1,413 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, tick } from 'svelte';
|
||||
|
||||
import type { key_val } from '$lib/ae_stores';
|
||||
import { ae_util } from '$lib/ae_utils';
|
||||
// import { api } from '$lib/api';
|
||||
import { core_func } from '$lib/ae_core_functions';
|
||||
import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/ae_stores';
|
||||
|
||||
export let element_id = 'svelte_input_file_element';
|
||||
export let input_name = 'file_list';
|
||||
export let container_class_li: string[] = [];
|
||||
export let input_class_li: string[] = ['file_drop_area'];
|
||||
export let table_class_li: string[] = ['table', 'table-sm', 'table-striped', 'table-hover' , 'text-sm'];
|
||||
|
||||
export let multiple: boolean = true;
|
||||
export let required: boolean = true;
|
||||
export let accept: string = 'audio/*, image/*, video/*, .bak, .cfg, .css, .csv, .doc, .docx, .gz, .htm, .html, .ini, .iso, .j2, .json, .key, .keynote, .md, .pdf, .ppt, .pptx, .rar, .rtf, .sql, .svelte, ttf, .txt, .xls, .xlsx, .xz, .zip, .bin, .dmg, .exe, .js, .msi, .php, .py, .sh';
|
||||
export let untrusted_extension_list = ['bin', 'dmg', 'exe', 'js', 'msi', 'php', 'py', 'sh'];
|
||||
export let legacy_extension_list = ['avi', 'doc', 'ppt', 'xls', 'wmv'];
|
||||
export let use_selected_file_table = true;
|
||||
|
||||
export let file_list_status: null|string = null;
|
||||
export let processed_file_list: any[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let input_file_list: any = null;
|
||||
let input_file_list_processed: any[] = [];
|
||||
|
||||
onMount(() => {
|
||||
console.log('** Element Mounted: ** Element Input File');
|
||||
});
|
||||
|
||||
|
||||
$: if (input_file_list) {
|
||||
console.log(input_file_list);
|
||||
|
||||
process_file_list(input_file_list)
|
||||
.then(function (result) {
|
||||
// console.log(result);
|
||||
|
||||
if (!result || !result.length) {
|
||||
file_list_status = 'none';
|
||||
}
|
||||
|
||||
// Save the results to the file upload list to be displayed as a table.
|
||||
input_file_list_processed = result; // Includes file hash
|
||||
|
||||
dispatch(
|
||||
'input_file_list_updated',
|
||||
{
|
||||
element_id: element_id,
|
||||
input_file_list: input_file_list,
|
||||
input_file_list_processed: result, // Includes file hash
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function process_file_list(file_list) {
|
||||
console.log('*** process_file_list() ***');
|
||||
|
||||
file_list_status = 'processing';
|
||||
processed_file_list = [];
|
||||
|
||||
if (!file_list) {
|
||||
// file_list_processed = null;
|
||||
file_list_status = 'none';
|
||||
// await tick();
|
||||
return processed_file_list;
|
||||
}
|
||||
|
||||
// const forLoop = async _ => {
|
||||
// console.log('*** Start ***');
|
||||
|
||||
// for (let [i, file_item] of file_list.entries()) { // Not sure why this does not work???
|
||||
for await (const [i, file_item] of Array.prototype.entries.call(file_list)) {
|
||||
console.log(i, file_item);
|
||||
|
||||
// NOTE: The file list is readonly. The filenames can not be changed here.
|
||||
if (file_item.name.endsWith('.odpmac') || file_item.name.endsWith('.odpwin') || file_item.name.endsWith('.pptmac') || file_item.name.endsWith('.pptwin') || file_item.name.endsWith('.pptxmac') || file_item.name.endsWith('.pptxwin')) {
|
||||
console.log('This file extension may need to be fixed? API upload will take care of it.');
|
||||
// file_item.name = file_item.name.replace('.odpwin', '.odp');
|
||||
}
|
||||
|
||||
let file_data: key_val = {};
|
||||
|
||||
let filename = file_item.name;
|
||||
// console.log(filename);
|
||||
file_data['filename'] = filename;
|
||||
|
||||
let guessed_extension = ae_util.guess_file_extension(filename);
|
||||
file_data['guessed_extension'] = guessed_extension;
|
||||
|
||||
file_data['type'] = file_item.type;
|
||||
|
||||
let modified_date = new Date(file_item.lastModified);
|
||||
file_data['modified_date'] = modified_date;
|
||||
let modified_datetime_string = ae_util.iso_datetime_formatter(modified_date, 'datetime_medium');
|
||||
file_data['modified_datetime_string'] = modified_datetime_string;
|
||||
|
||||
let file_size_bytes = file_item.size;
|
||||
file_data['file_size_bytes'] = file_size_bytes;
|
||||
let file_size_string = ae_util.format_bytes(file_item.size, 2);
|
||||
file_data['file_size_string'] = file_size_string;
|
||||
|
||||
// // NOTE: Calculate the hash of the file before upload. Check if this exact file has already been uploaded.
|
||||
// let file_reader = new FileReader();
|
||||
// file_reader.onload = async function() {
|
||||
|
||||
// const hash_buffer = crypto.subtle.digest('SHA-256', file_reader.result);
|
||||
// let hash_hex_test = hash_buffer.then(async function (result_buffer) {
|
||||
// const hash_array = Array.from(new Uint8Array(result_buffer)); // convert buffer to byte array
|
||||
// const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// console.log(`File hash hex? ${hash_hex}`);
|
||||
// file_data['hash_sha256'] = hash_hex;
|
||||
// return hash_hex;
|
||||
// })
|
||||
// .catch(function (error) {
|
||||
// console.log('Something went wrong?', error);
|
||||
// });
|
||||
|
||||
// return hash_hex_test;
|
||||
|
||||
// // file_data['hash_hex_test'] = hash_hex_test;
|
||||
|
||||
// // const hash_buffer = await crypto.subtle.digest('SHA-256', file_reader.result);
|
||||
// // let hash_str = await new TextDecoder().decode(hash_buffer);
|
||||
// // console.log(`File hash string? ${hash_str}`);
|
||||
// // file_data['hash_str'] = hash_str;
|
||||
|
||||
// // const hash_array = Array.from(new Uint8Array(hash_buffer)); // convert buffer to byte array
|
||||
// // const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// // console.log(`File hash hex? ${hash_hex}`);
|
||||
|
||||
// // file_data['hash_sha256'] = hash_hex;
|
||||
// // return hash_hex_test;
|
||||
// // return hash_hex;
|
||||
// }
|
||||
|
||||
// // file_reader.then(function (result) {
|
||||
// // console.log(`File hash hex? ${result}`);
|
||||
// // return hash_hex;
|
||||
// // })
|
||||
// // .catch(function (error) {
|
||||
// // console.log('No results returned or failed.', error);
|
||||
// // });
|
||||
// // file_data['hash_sha256'] = file_reader.readAsArrayBuffer(file_item);
|
||||
|
||||
|
||||
|
||||
let warning_untrusted_extension = false;
|
||||
let warning_legacy_extension = false;
|
||||
let warning_size = false;
|
||||
let warning_message = null;
|
||||
|
||||
if (untrusted_extension_list.includes(guessed_extension)) {
|
||||
console.log('This is an untrusted extension. Going to warn.')
|
||||
warning_untrusted_extension = true;
|
||||
warning_message = 'It appears this an untrusted file type and is likely meant to be an executable or installable application. It is <strong>strongly</strong> recommended that this file not be used.';
|
||||
|
||||
} else if (legacy_extension_list.includes(guessed_extension)) {
|
||||
console.log('This is a legacy extension. Going to warn.')
|
||||
warning_legacy_extension = true;
|
||||
if (guessed_extension == 'ppt') {
|
||||
warning_message = 'It appears this is a legacy PowerPoint file and has not been officially supported since Office PowerPoint 2003. This file is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern PPTX format.';
|
||||
} else if (guessed_extension == 'avi') {
|
||||
warning_message = 'It appears this is a video file using the AVI format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG. The file will also likely be much smaller.';
|
||||
} else if (guessed_extension == 'wmv') {
|
||||
warning_message = 'It appears this is a video file using Microsoft\'s WMV format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG.';
|
||||
} else {
|
||||
warning_message = 'It appears this is a legacy or not very well supported file format. It is <strong>strongly</strong> recommended that it be saved in an alternative format if possible.';
|
||||
}
|
||||
|
||||
} else if (file_size_bytes > 52428800) {
|
||||
// 50 MB = 52428800 bytes
|
||||
// 100 MB = 104857600 bytes
|
||||
console.log(`This is a large file size ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes). Going to warn.`);
|
||||
warning_size = true;
|
||||
if (file_size_bytes > 2147483648) { // > 2 GB
|
||||
warning_message = `This file size (${file_size_string}) is very large and will take at <strong>least</strong> a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Most audio, image, and video files can be compressed without a significant loss in quality. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly.`;
|
||||
} else if (file_size_bytes > 209715200) { // > 200 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and will likely take at <strong>least</strong> a few minutes to upload depending on your network connection. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
} else if (file_size_bytes > 104857600) { // 100 MB to 200 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and will likely take a few minutes to upload depending on your network connection. Be sure you have a stable network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
} else { // 50 to 100 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and may take a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
}
|
||||
|
||||
} else {
|
||||
}
|
||||
|
||||
file_data['warning_untrusted_extension'] = warning_untrusted_extension;
|
||||
file_data['warning_legacy_extension'] = warning_legacy_extension;
|
||||
file_data['warning_size'] = warning_size;
|
||||
|
||||
file_data['warning_message'] = warning_message;
|
||||
|
||||
file_data['uploaded'] = null;
|
||||
file_data['uploaded_bytes'] = null;
|
||||
|
||||
// input_file_list_processed.push(JSON.parse(JSON.stringify(file_data)));
|
||||
// input_file_list_processed = input_file_list_processed;
|
||||
|
||||
// console.log(get_file_hash(file_item));
|
||||
// console.log(await get_file_hash(file_item));
|
||||
|
||||
let file_hash = null;
|
||||
|
||||
// Only hash files less than 2 GB (2147483648 bytes)!!!
|
||||
console.log(`File size: ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes)`);
|
||||
if (file_size_bytes < 2000000000) { // > 2 GB 2 147 483 648
|
||||
file_hash = await ae_util.get_file_hash(file_item);
|
||||
} else {
|
||||
// File size in MB
|
||||
console.log(`File is too large to hash. File size: ${file_size_bytes / 1048576} MB`);
|
||||
}
|
||||
|
||||
|
||||
if (file_hash) {
|
||||
console.log(`Found file hash to lookup: ${ae_util.shorten_string({string: file_hash})}`);
|
||||
file_data['hash_sha256'] = file_hash;
|
||||
|
||||
let check_hosted_file_obj_w_hash_result = await core_func.check_hosted_file_obj_w_hash({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_hash: file_hash
|
||||
});
|
||||
|
||||
// console.log(check_hosted_file_obj_w_hash_result);
|
||||
|
||||
if (check_hosted_file_obj_w_hash_result && check_hosted_file_obj_w_hash_result.hosted_file_found_check) {
|
||||
console.log('Matching hash!!!');
|
||||
file_data['hash_sha256_match'] = true;
|
||||
// $ae_events.pres_mgmt.new_upload_list[i].hash_sha256_match = true;
|
||||
// $ae_events = $ae_events;
|
||||
}
|
||||
} else {
|
||||
file_data['hash_sha256'] = null;
|
||||
file_data['hash_sha256_match'] = false;
|
||||
}
|
||||
|
||||
processed_file_list.push(file_data);
|
||||
// input_file_list_processed.push(file_data);
|
||||
}
|
||||
|
||||
file_list_status = 'ready';
|
||||
console.log(processed_file_list);
|
||||
|
||||
// return JSON.parse(JSON.stringify(processed_file_list));
|
||||
return processed_file_list
|
||||
}
|
||||
|
||||
|
||||
function remove_file_from_filelist(index) {
|
||||
console.log('*** remove_file_from_filelist() ***');
|
||||
|
||||
// Can not use something like this because it is readonly:
|
||||
const dt = new DataTransfer();
|
||||
// let input = document.getElementById(input_element_id);
|
||||
|
||||
let input_element = document.querySelector('input[type="file"].svelte_input_file_element');
|
||||
|
||||
if (!input_element) {
|
||||
console.error('Could not find the input element.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let files = input_file_list;
|
||||
|
||||
if (!files || !files.length) {
|
||||
console.error('No files found in the file list.');
|
||||
file_list_status = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (index !== i) // Only include the file if it does not match the index value.
|
||||
dt.items.add(file);
|
||||
}
|
||||
|
||||
// NOTE: I thought just setting the input_element.files OR input_file_list would trigger the input_file_list change.
|
||||
// input_element.files = Object.assign({}, dt.files);
|
||||
input_element.files = dt.files; // Assign the updates list
|
||||
// input_file_list = null;
|
||||
input_file_list = dt.files; // I feel like this should not need to be done, but doing it anyways.
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="svelte_element ae_element ae_input_file flex flex-col gap-1 items-center justify-center {container_class_li.join(' ')} text-center">
|
||||
<label
|
||||
for={element_id}
|
||||
class="svelte_input_file_label text-center"
|
||||
>
|
||||
<div>
|
||||
<span class="fas fa-upload"></span>
|
||||
<!-- Select files to upload -->
|
||||
<!-- <span class="fas fa-file-archive"></span> -->
|
||||
<strong>Upload your files</strong>
|
||||
<!-- (drag and drop) -->
|
||||
</div>
|
||||
<span>
|
||||
Presentation related files only<br>
|
||||
(PowerPoint, Keynote, PDF, mp4, Word Doc, Excel, txt, etc)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
id={element_id}
|
||||
type="file"
|
||||
bind:files={input_file_list}
|
||||
{multiple}
|
||||
{required}
|
||||
{accept}
|
||||
name={input_name}
|
||||
class="svelte_input_file_element {input_class_li.join(' ')}"
|
||||
/>
|
||||
|
||||
{#if file_list_status == 'processing'}
|
||||
<div class="file_list_status ae_warning variant-ghost-warning p-1 m-1">
|
||||
<span class="fas fa-spinner fa-spin m-1"></span> Processing selected file list...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- {#await input_file_list_processed} -->
|
||||
<!-- {:then} -->
|
||||
{#if use_selected_file_table && input_file_list_processed && input_file_list_processed.length}
|
||||
<strong>Files selected for upload</strong>
|
||||
<table class="slct_file_list text-sm {table_class_li.join(' ')}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Remove</th>
|
||||
<th>Filename</th>
|
||||
<th>Modified</th>
|
||||
<th>Size</th>
|
||||
<!-- <th>Type</th> -->
|
||||
<th>Ext</th>
|
||||
<th>Hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{#each input_file_list_processed as file_list_item, file_index}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="file_remove">
|
||||
<button
|
||||
on:click|preventDefault={() => { (remove_file_from_filelist(file_index)); }}
|
||||
class="btn btn-md variant-soft-warning hover:variant-filled-secondary m-1"
|
||||
title="Remove file from upload list">
|
||||
<span class="fas fa-minus"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="file_filename">{file_list_item.filename}</td>
|
||||
<td class="file_last_modified">{file_list_item.modified_datetime_string}</td>
|
||||
<td
|
||||
class="file_size"
|
||||
class:bg-pink-200={file_list_item.warning_size}
|
||||
>
|
||||
{file_list_item.file_size_string}
|
||||
<!-- {#if $ae_sess.api_upload_kv[link_to_id]}
|
||||
<span class="text-xs">({$ae_sess.api_upload_kv[link_to_id].percent_completed}%)</span>
|
||||
{/if} -->
|
||||
</td>
|
||||
<!-- <td class="file_type" class:warning_file_untrusted_extension={file_list_item.warning_untrusted_extension} class:warning_file_legacy_extension={file_list_item.warning_legacy_extension}>{file_list_item.type}</td> -->
|
||||
<td
|
||||
class="file_extension"
|
||||
class:bg-red-200={file_list_item.warning_untrusted_extension}
|
||||
class:bg-pink-200={file_list_item.warning_legacy_extension}
|
||||
>
|
||||
{file_list_item.guessed_extension}
|
||||
</td>
|
||||
<td
|
||||
class="file_hash file_hash256"
|
||||
class:bg-pink-200={file_list_item.hash_sha256_match}
|
||||
>
|
||||
{ae_util.shorten_string({string: file_list_item.hash_sha256, begin_length: 5, end_length: 4, wildcard_length: 2})}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
th {
|
||||
text-align: center;
|
||||
/* font-size: smaller; */
|
||||
}
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
.file_last_modified {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.file_size, .file_type {
|
||||
/* font-size: smaller; */
|
||||
}
|
||||
|
||||
.file_hash {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
</style>
|
||||
382
src/lib/element_input_files_tbl.svelte
Normal file
382
src/lib/element_input_files_tbl.svelte
Normal file
@@ -0,0 +1,382 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, tick } from 'svelte';
|
||||
|
||||
import type { key_val } from '$lib/ae_stores';
|
||||
import { ae_util } from '$lib/ae_utils';
|
||||
// import { api } from '$lib/api';
|
||||
import { core_func } from '$lib/ae_core_functions';
|
||||
import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/ae_stores';
|
||||
|
||||
export let element_id = 'svelte_input_file_element';
|
||||
export let container_class_li: string[] = [];
|
||||
export let table_class_li: string[] = ['table', 'table-sm', 'table-striped', 'table-hover' , 'text-sm'];
|
||||
|
||||
|
||||
export let untrusted_extension_list = ['bin', 'dmg', 'exe', 'js', 'msi', 'php', 'py', 'sh'];
|
||||
export let legacy_extension_list = ['avi', 'doc', 'ppt', 'xls', 'wmv'];
|
||||
export let use_selected_file_table = true;
|
||||
|
||||
export let input_file_list: any = null;
|
||||
export let file_list_status: null|string = null;
|
||||
export let processed_file_list: any[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let input_file_list_processed: any[] = [];
|
||||
|
||||
onMount(() => {
|
||||
console.log('** Element Mounted: ** Element Input File');
|
||||
});
|
||||
|
||||
|
||||
$: if (input_file_list) {
|
||||
console.log(input_file_list);
|
||||
|
||||
process_file_list(input_file_list)
|
||||
.then(function (result) {
|
||||
// console.log(result);
|
||||
|
||||
if (!result || !result.length) {
|
||||
file_list_status = 'none';
|
||||
}
|
||||
|
||||
// Save the results to the file upload list to be displayed as a table.
|
||||
input_file_list_processed = result; // Includes file hash
|
||||
|
||||
dispatch(
|
||||
'input_file_list_updated',
|
||||
{
|
||||
element_id: element_id,
|
||||
input_file_list: input_file_list,
|
||||
input_file_list_processed: result, // Includes file hash
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function process_file_list(file_list) {
|
||||
console.log('*** process_file_list() ***');
|
||||
|
||||
file_list_status = 'processing';
|
||||
processed_file_list = [];
|
||||
|
||||
if (!file_list) {
|
||||
// file_list_processed = null;
|
||||
file_list_status = 'none';
|
||||
// await tick();
|
||||
return processed_file_list;
|
||||
}
|
||||
|
||||
// const forLoop = async _ => {
|
||||
// console.log('*** Start ***');
|
||||
|
||||
// for (let [i, file_item] of file_list.entries()) { // Not sure why this does not work???
|
||||
for await (const [i, file_item] of Array.prototype.entries.call(file_list)) {
|
||||
console.log(i, file_item);
|
||||
|
||||
// NOTE: The file list is readonly. The filenames can not be changed here.
|
||||
if (file_item.name.endsWith('.odpmac') || file_item.name.endsWith('.odpwin') || file_item.name.endsWith('.pptmac') || file_item.name.endsWith('.pptwin') || file_item.name.endsWith('.pptxmac') || file_item.name.endsWith('.pptxwin')) {
|
||||
console.log('This file extension may need to be fixed? API upload will take care of it.');
|
||||
// file_item.name = file_item.name.replace('.odpwin', '.odp');
|
||||
}
|
||||
|
||||
let file_data: key_val = {};
|
||||
|
||||
let filename = file_item.name;
|
||||
// console.log(filename);
|
||||
file_data['filename'] = filename;
|
||||
|
||||
let guessed_extension = ae_util.guess_file_extension(filename);
|
||||
file_data['guessed_extension'] = guessed_extension;
|
||||
|
||||
file_data['type'] = file_item.type;
|
||||
|
||||
let modified_date = new Date(file_item.lastModified);
|
||||
file_data['modified_date'] = modified_date;
|
||||
let modified_datetime_string = ae_util.iso_datetime_formatter(modified_date, 'datetime_medium');
|
||||
file_data['modified_datetime_string'] = modified_datetime_string;
|
||||
|
||||
let file_size_bytes = file_item.size;
|
||||
file_data['file_size_bytes'] = file_size_bytes;
|
||||
let file_size_string = ae_util.format_bytes(file_item.size, 2);
|
||||
file_data['file_size_string'] = file_size_string;
|
||||
|
||||
// // NOTE: Calculate the hash of the file before upload. Check if this exact file has already been uploaded.
|
||||
// let file_reader = new FileReader();
|
||||
// file_reader.onload = async function() {
|
||||
|
||||
// const hash_buffer = crypto.subtle.digest('SHA-256', file_reader.result);
|
||||
// let hash_hex_test = hash_buffer.then(async function (result_buffer) {
|
||||
// const hash_array = Array.from(new Uint8Array(result_buffer)); // convert buffer to byte array
|
||||
// const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// console.log(`File hash hex? ${hash_hex}`);
|
||||
// file_data['hash_sha256'] = hash_hex;
|
||||
// return hash_hex;
|
||||
// })
|
||||
// .catch(function (error) {
|
||||
// console.log('Something went wrong?', error);
|
||||
// });
|
||||
|
||||
// return hash_hex_test;
|
||||
|
||||
// // file_data['hash_hex_test'] = hash_hex_test;
|
||||
|
||||
// // const hash_buffer = await crypto.subtle.digest('SHA-256', file_reader.result);
|
||||
// // let hash_str = await new TextDecoder().decode(hash_buffer);
|
||||
// // console.log(`File hash string? ${hash_str}`);
|
||||
// // file_data['hash_str'] = hash_str;
|
||||
|
||||
// // const hash_array = Array.from(new Uint8Array(hash_buffer)); // convert buffer to byte array
|
||||
// // const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// // console.log(`File hash hex? ${hash_hex}`);
|
||||
|
||||
// // file_data['hash_sha256'] = hash_hex;
|
||||
// // return hash_hex_test;
|
||||
// // return hash_hex;
|
||||
// }
|
||||
|
||||
// // file_reader.then(function (result) {
|
||||
// // console.log(`File hash hex? ${result}`);
|
||||
// // return hash_hex;
|
||||
// // })
|
||||
// // .catch(function (error) {
|
||||
// // console.log('No results returned or failed.', error);
|
||||
// // });
|
||||
// // file_data['hash_sha256'] = file_reader.readAsArrayBuffer(file_item);
|
||||
|
||||
|
||||
|
||||
let warning_untrusted_extension = false;
|
||||
let warning_legacy_extension = false;
|
||||
let warning_size = false;
|
||||
let warning_message = null;
|
||||
|
||||
if (untrusted_extension_list.includes(guessed_extension)) {
|
||||
console.log('This is an untrusted extension. Going to warn.')
|
||||
warning_untrusted_extension = true;
|
||||
warning_message = 'It appears this an untrusted file type and is likely meant to be an executable or installable application. It is <strong>strongly</strong> recommended that this file not be used.';
|
||||
|
||||
} else if (legacy_extension_list.includes(guessed_extension)) {
|
||||
console.log('This is a legacy extension. Going to warn.')
|
||||
warning_legacy_extension = true;
|
||||
if (guessed_extension == 'ppt') {
|
||||
warning_message = 'It appears this is a legacy PowerPoint file and has not been officially supported since Office PowerPoint 2003. This file is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern PPTX format.';
|
||||
} else if (guessed_extension == 'avi') {
|
||||
warning_message = 'It appears this is a video file using the AVI format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG. The file will also likely be much smaller.';
|
||||
} else if (guessed_extension == 'wmv') {
|
||||
warning_message = 'It appears this is a video file using Microsoft\'s WMV format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG.';
|
||||
} else {
|
||||
warning_message = 'It appears this is a legacy or not very well supported file format. It is <strong>strongly</strong> recommended that it be saved in an alternative format if possible.';
|
||||
}
|
||||
|
||||
} else if (file_size_bytes > 52428800) {
|
||||
// 50 MB = 52428800 bytes
|
||||
// 100 MB = 104857600 bytes
|
||||
console.log(`This is a large file size ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes). Going to warn.`);
|
||||
warning_size = true;
|
||||
if (file_size_bytes > 2147483648) { // > 2 GB
|
||||
warning_message = `This file size (${file_size_string}) is very large and will take at <strong>least</strong> a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Most audio, image, and video files can be compressed without a significant loss in quality. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly.`;
|
||||
} else if (file_size_bytes > 209715200) { // > 200 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and will likely take at <strong>least</strong> a few minutes to upload depending on your network connection. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
} else if (file_size_bytes > 104857600) { // 100 MB to 200 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and will likely take a few minutes to upload depending on your network connection. Be sure you have a stable network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
} else { // 50 to 100 MB
|
||||
warning_message = `This file size (${file_size_string}) is large and may take a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
||||
}
|
||||
|
||||
} else {
|
||||
}
|
||||
|
||||
file_data['warning_untrusted_extension'] = warning_untrusted_extension;
|
||||
file_data['warning_legacy_extension'] = warning_legacy_extension;
|
||||
file_data['warning_size'] = warning_size;
|
||||
|
||||
file_data['warning_message'] = warning_message;
|
||||
|
||||
file_data['uploaded'] = null;
|
||||
file_data['uploaded_bytes'] = null;
|
||||
|
||||
// input_file_list_processed.push(JSON.parse(JSON.stringify(file_data)));
|
||||
// input_file_list_processed = input_file_list_processed;
|
||||
|
||||
// console.log(get_file_hash(file_item));
|
||||
// console.log(await get_file_hash(file_item));
|
||||
|
||||
let file_hash = null;
|
||||
|
||||
// Only hash files less than 2 GB (2147483648 bytes)!!!
|
||||
console.log(`File size: ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes)`);
|
||||
if (file_size_bytes < 2000000000) { // > 2 GB 2 147 483 648
|
||||
file_hash = await ae_util.get_file_hash(file_item);
|
||||
} else {
|
||||
// File size in MB
|
||||
console.log(`File is too large to hash. File size: ${file_size_bytes / 1048576} MB`);
|
||||
}
|
||||
|
||||
|
||||
if (file_hash) {
|
||||
console.log(`Found file hash to lookup: ${ae_util.shorten_string({string: file_hash})}`);
|
||||
file_data['hash_sha256'] = file_hash;
|
||||
|
||||
let check_hosted_file_obj_w_hash_result = await core_func.check_hosted_file_obj_w_hash({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_hash: file_hash
|
||||
});
|
||||
|
||||
// console.log(check_hosted_file_obj_w_hash_result);
|
||||
|
||||
if (check_hosted_file_obj_w_hash_result && check_hosted_file_obj_w_hash_result.hosted_file_found_check) {
|
||||
console.log('Matching hash!!!');
|
||||
file_data['hash_sha256_match'] = true;
|
||||
// $ae_events.pres_mgmt.new_upload_list[i].hash_sha256_match = true;
|
||||
// $ae_events = $ae_events;
|
||||
}
|
||||
} else {
|
||||
file_data['hash_sha256'] = null;
|
||||
file_data['hash_sha256_match'] = false;
|
||||
}
|
||||
|
||||
processed_file_list.push(file_data);
|
||||
// input_file_list_processed.push(file_data);
|
||||
}
|
||||
|
||||
file_list_status = 'ready';
|
||||
console.log(processed_file_list);
|
||||
|
||||
// return JSON.parse(JSON.stringify(processed_file_list));
|
||||
return processed_file_list
|
||||
}
|
||||
|
||||
|
||||
function remove_file_from_filelist(index) {
|
||||
console.log('*** remove_file_from_filelist() ***');
|
||||
|
||||
// Can not use something like this because it is readonly:
|
||||
const dt = new DataTransfer();
|
||||
// let input = document.getElementById(input_element_id);
|
||||
|
||||
let input_element = document.querySelector('input[type="file"].svelte_input_file_element');
|
||||
|
||||
if (!input_element) {
|
||||
console.error('Could not find the input element.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let files = input_file_list;
|
||||
|
||||
if (!files || !files.length) {
|
||||
console.error('No files found in the file list.');
|
||||
file_list_status = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (index !== i) // Only include the file if it does not match the index value.
|
||||
dt.items.add(file);
|
||||
}
|
||||
|
||||
// NOTE: I thought just setting the input_element.files OR input_file_list would trigger the input_file_list change.
|
||||
// input_element.files = Object.assign({}, dt.files);
|
||||
input_element.files = dt.files; // Assign the updates list
|
||||
// input_file_list = null;
|
||||
input_file_list = dt.files; // I feel like this should not need to be done, but doing it anyways.
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="svelte_element ae_element ae_input_file flex flex-col gap-1 items-center justify-center {container_class_li.join(' ')} text-center">
|
||||
|
||||
|
||||
{#if file_list_status == 'processing'}
|
||||
<div class="file_list_status ae_warning variant-ghost-warning p-1 m-1">
|
||||
<span class="fas fa-spinner fa-spin m-1"></span> Processing selected file list...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- {#await input_file_list_processed} -->
|
||||
<!-- {:then} -->
|
||||
{#if use_selected_file_table && input_file_list_processed && input_file_list_processed.length}
|
||||
<strong>Files selected for upload</strong>
|
||||
<table class="slct_file_list text-sm {table_class_li.join(' ')}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Remove</th>
|
||||
<th>Filename</th>
|
||||
<th>Modified</th>
|
||||
<th>Size</th>
|
||||
<!-- <th>Type</th> -->
|
||||
<th>Ext</th>
|
||||
<th>Hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{#each input_file_list_processed as file_list_item, file_index}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="file_remove">
|
||||
<button
|
||||
on:click|preventDefault={() => { (remove_file_from_filelist(file_index)); }}
|
||||
class="btn btn-md variant-soft-warning hover:variant-filled-secondary m-1"
|
||||
title="Remove file from upload list">
|
||||
<span class="fas fa-minus"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="file_filename">{file_list_item.filename}</td>
|
||||
<td class="file_last_modified">{file_list_item.modified_datetime_string}</td>
|
||||
<td
|
||||
class="file_size"
|
||||
class:bg-pink-200={file_list_item.warning_size}
|
||||
>
|
||||
{file_list_item.file_size_string}
|
||||
<!-- {#if $ae_sess.api_upload_kv[link_to_id]}
|
||||
<span class="text-xs">({$ae_sess.api_upload_kv[link_to_id].percent_completed}%)</span>
|
||||
{/if} -->
|
||||
</td>
|
||||
<!-- <td class="file_type" class:warning_file_untrusted_extension={file_list_item.warning_untrusted_extension} class:warning_file_legacy_extension={file_list_item.warning_legacy_extension}>{file_list_item.type}</td> -->
|
||||
<td
|
||||
class="file_extension"
|
||||
class:bg-red-200={file_list_item.warning_untrusted_extension}
|
||||
class:bg-pink-200={file_list_item.warning_legacy_extension}
|
||||
>
|
||||
{file_list_item.guessed_extension}
|
||||
</td>
|
||||
<td
|
||||
class="file_hash file_hash256"
|
||||
class:bg-pink-200={file_list_item.hash_sha256_match}
|
||||
>
|
||||
{ae_util.shorten_string({string: file_list_item.hash_sha256, begin_length: 5, end_length: 4, wildcard_length: 2})}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
th {
|
||||
text-align: center;
|
||||
/* font-size: smaller; */
|
||||
}
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
.file_last_modified {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.file_size, .file_type {
|
||||
/* font-size: smaller; */
|
||||
}
|
||||
|
||||
.file_hash {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user