mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-15 21:04:46 +01:00
feature (table): table opener app
This commit is contained in:
parent
2b872a07be
commit
2ac816dc9c
12 changed files with 387 additions and 24 deletions
|
|
@ -4,6 +4,7 @@
|
|||
"3gp": "video/3gpp",
|
||||
"3gpp": "video/3gpp",
|
||||
"7z": "application/x-7z-compressed",
|
||||
"a": "application/x-archive",
|
||||
"ai": "application/pdf",
|
||||
"aif": "audio/x-aiff",
|
||||
"aiff": "audio/x-aiff",
|
||||
|
|
@ -169,6 +170,7 @@
|
|||
"war": "application/java-archive",
|
||||
"wav": "audio/wave",
|
||||
"wave": "audio/wave",
|
||||
"wasm": "application/wasm",
|
||||
"wbmp": "image/vnd.wap.wbmp",
|
||||
"webm": "video/webm",
|
||||
"webp": "image/webp",
|
||||
|
|
|
|||
172
public/assets/helpers/loader_wasm.js
Normal file
172
public/assets/helpers/loader_wasm.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
export default async function(baseURL, path) {
|
||||
const wasi = new Wasi();
|
||||
const wasm = await WebAssembly.instantiateStreaming(
|
||||
fetch(new URL(path, baseURL)), {
|
||||
wasi_snapshot_preview1: {
|
||||
...wasi,
|
||||
},
|
||||
env: {
|
||||
...wasi,
|
||||
...syscalls,
|
||||
},
|
||||
},
|
||||
);
|
||||
wasi.instance = wasm.instance;
|
||||
return wasm;
|
||||
}
|
||||
|
||||
const FS = {};
|
||||
let nextFd = 0;
|
||||
writeFS(new Uint8Array(), 0); // stdin
|
||||
writeFS(new Uint8Array(), 1); // stdout
|
||||
writeFS(new Uint8Array(), 2); // stderr
|
||||
if (nextFd !== 3) throw new Error("Unexpected next fd");
|
||||
|
||||
export function writeFS(buffer, fd) {
|
||||
if (fd === undefined) fd = nextFd;
|
||||
else if (!(buffer instanceof Uint8Array)) throw new Error("can only write Uint8Array");
|
||||
|
||||
FS[fd] = {
|
||||
buffer,
|
||||
position: 0,
|
||||
};
|
||||
nextFd += 1;
|
||||
return nextFd - 1;
|
||||
}
|
||||
|
||||
export function readFS(fd) {
|
||||
if (fd < 3) throw new Error("cannot read from stdin, stdout or stderr");
|
||||
const file = FS[fd];
|
||||
if (!file) throw new Error("file does not exist");
|
||||
return file.buffer;
|
||||
}
|
||||
|
||||
export const syscalls = {
|
||||
__syscall_fcntl64: (fd, cmd, varargs) => {
|
||||
console.log(`Stubbed __syscall_fcntl64 called with fd=${fd}, cmd=${cmd}, varargs=${varargs}`);
|
||||
return -1;
|
||||
},
|
||||
__syscall_ioctl: (fd, op, varargs) => {
|
||||
switch (op) {
|
||||
case 21523:
|
||||
break;
|
||||
default:
|
||||
console.log(`Stubbed __syscall_ioctl called with fd=${fd}, request=${op}, varargs=${varargs}`);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
export class Wasi {
|
||||
#instance;
|
||||
|
||||
constructor() {
|
||||
this.fd_read = this.fd_read.bind(this);
|
||||
this.fd_write = this.fd_write.bind(this);
|
||||
this.fd_seek = this.fd_seek.bind(this);
|
||||
this.fd_close = this.fd_close.bind(this);
|
||||
}
|
||||
|
||||
set instance(val) {
|
||||
this.#instance = val;
|
||||
}
|
||||
|
||||
fd_write(fd, iovs, iovs_len, nwritten) {
|
||||
if (!FS[fd]) {
|
||||
console.error(`Invalid fd: ${fd}`);
|
||||
return -1;
|
||||
}
|
||||
let output = FS[fd].buffer;
|
||||
const ioVecArray = new Uint32Array(this.#instance.exports.memory.buffer, iovs, iovs_len * 2);
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
let totalBytesWritten = 0;
|
||||
for (let i = 0; i < iovs_len * 2; i += 2) {
|
||||
const sub = memory.subarray(
|
||||
(ioVecArray[i] || 0),
|
||||
(ioVecArray[i] || 0) + (ioVecArray[i+1] || 0),
|
||||
);
|
||||
const tmp = new Uint8Array(output.byteLength + sub.byteLength);
|
||||
tmp.set(output, 0);
|
||||
tmp.set(sub, output.byteLength);
|
||||
output = tmp;
|
||||
totalBytesWritten += ioVecArray[i+1] || 0;
|
||||
}
|
||||
const dataView = new DataView(this.#instance.exports.memory.buffer);
|
||||
dataView.setUint32(nwritten, totalBytesWritten, true);
|
||||
|
||||
FS[fd].buffer = output;
|
||||
if (fd < 3 && fd >= 0) {
|
||||
const msg = fd === 1 ? "stdout" : fd === 2 ? "stderr" : "stdxx";
|
||||
console.log(msg + ": " + (new TextDecoder()).decode(output));
|
||||
FS[fd].buffer = new ArrayBuffer(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fd_read(fd, iovs, iovs_len, nread) {
|
||||
const file = FS[fd];
|
||||
if (!file) {
|
||||
console.error(`Invalid fd: ${fd}`);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const ioVecArray = new Uint32Array(this.#instance.exports.memory.buffer, iovs, iovs_len * 2);
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
let totalBytesRead = 0;
|
||||
for (let i = 0; i < iovs_len * 2; i += 2) {
|
||||
const offset = ioVecArray[i];
|
||||
const length = ioVecArray[i + 1] || 0;
|
||||
const bytesToRead = Math.min(
|
||||
length,
|
||||
file.buffer.length - file.position,
|
||||
);
|
||||
if (bytesToRead <= 0) {
|
||||
break;
|
||||
}
|
||||
memory.set(
|
||||
file.buffer.subarray(file.position, file.position + bytesToRead),
|
||||
offset,
|
||||
);
|
||||
file.position += bytesToRead;
|
||||
totalBytesRead += bytesToRead;
|
||||
}
|
||||
|
||||
const dataView = new DataView(this.#instance.exports.memory.buffer);
|
||||
dataView.setUint32(nread, totalBytesRead, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fd_seek(fd, offsetBigInt, whence) {
|
||||
const offset = Number(offsetBigInt);
|
||||
const file = FS[fd];
|
||||
if (!file) {
|
||||
console.error(`Invalid FD: ${fd}`);
|
||||
return -1;
|
||||
}
|
||||
switch (whence) {
|
||||
case 0: // SEEK_SET
|
||||
file.position = offset;
|
||||
break;
|
||||
case 1: // SEEK_CUR
|
||||
file.position += offset;
|
||||
break;
|
||||
case 2: // SEEK_END
|
||||
file.position = file.buffer.length + offset;
|
||||
break;
|
||||
default:
|
||||
console.log(`fd_seek called with fd=${fd}, offset=${offset}, position=${file.position} whence=${whence}`);
|
||||
const error = new Error("fd_seek trace");
|
||||
console.log("Invalid whence", error.stack);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fd_close(fd) {
|
||||
if (!FS[fd]) {
|
||||
console.error(`Invalid FD: ${fd}`);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -181,9 +181,9 @@ export default async function(render) {
|
|||
$list,
|
||||
virtual,
|
||||
}) => (
|
||||
virtual ?
|
||||
rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }) :
|
||||
rxjs.EMPTY
|
||||
virtual
|
||||
? rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true })
|
||||
: rxjs.EMPTY
|
||||
).pipe(
|
||||
rxjs.map((e) => {
|
||||
// 0-------------0-----------1-----------2-----------3 ....
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import rxjs, { effect } from "../../lib/rx.js";
|
|||
import { qs, qsa } from "../../lib/dom.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { loadCSS } from "../../helpers/loader.js";
|
||||
import t from "../../locales/index.js";
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
|
||||
import { renderMenubar, buttonDownload } from "./component_menubar.js";
|
||||
import { getLoader } from "./application_table/loader.js";
|
||||
import { transition } from "./common.js";
|
||||
|
||||
export default async function(render, { getDownloadUrl = nop, getFilename = nop, hasMenubar = true }) {
|
||||
export default async function(render, { mime, getDownloadUrl = nop, getFilename = nop, hasMenubar = true }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_tableviewer">
|
||||
<component-menubar filename="${getFilename()}" class="${!hasMenubar && "hidden"}"></component-menubar>
|
||||
|
|
@ -33,29 +36,21 @@ export default async function(render, { getDownloadUrl = nop, getFilename = nop,
|
|||
// feature: initial render
|
||||
const init$ = ajax({ url: getDownloadUrl(), responseType: "arraybuffer" }).pipe(
|
||||
rxjs.mergeMap(async({ response }) => {
|
||||
const module = await import("../../lib/vendor/shp-to-geojson.browser.js");
|
||||
const data = new module.DBase(module.Buffer.from(response));
|
||||
|
||||
const dummy = [];
|
||||
// const dummy = new Array(50).fill({}).map((_, i) => ({
|
||||
// fieldName: "dummy"+i,
|
||||
// fieldLength: 4,
|
||||
// }));
|
||||
data.properties = data.properties.concat(dummy);
|
||||
const table = new (await getLoader(mime))(response);
|
||||
|
||||
// build head
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
data.properties.forEach(({ fieldName, fieldLength }) => {
|
||||
table.getHeader().forEach(({ name, size }) => {
|
||||
const $th = createElement(`
|
||||
<div title="${fieldName}" class="${withCenter("th ellipsis", fieldLength)}" style="${styleCell(fieldLength, fieldName, padding)}">
|
||||
${fieldName}
|
||||
<div title="${name}" class="${withCenter("th ellipsis", size)}" style="${styleCell(size, name, padding)}">
|
||||
${name}
|
||||
<img class="no-select" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" />
|
||||
</div>
|
||||
`);
|
||||
let ascending = null;
|
||||
qs($th, "img").onclick = (e) => {
|
||||
ascending = !ascending;
|
||||
sortBy(qsa($dom.tbody, `.tr [data-column="${fieldName}"]`), ascending);
|
||||
sortBy(qsa($dom.tbody, `.tr [data-column="${name}"]`), ascending);
|
||||
qsa(e.target.closest(".tr"), "img").forEach(($img) => {
|
||||
$img.style.transform = "rotate(0deg)";
|
||||
});
|
||||
|
|
@ -66,18 +61,24 @@ export default async function(render, { getDownloadUrl = nop, getFilename = nop,
|
|||
$dom.thead.appendChild($tr);
|
||||
|
||||
// build body
|
||||
for (let i =0; i<data.recordLength; i++) {
|
||||
const row = data.getRowProperties(i);
|
||||
const body = table.getBody();
|
||||
body.forEach((obj) => {
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
data.properties.forEach(({ fieldName, fieldLength }) => {
|
||||
table.getHeader().forEach(({ name, size }) => {
|
||||
$tr.appendChild(createElement(`
|
||||
<div data-column="${fieldName}" title="${row[fieldName]}" class="${withCenter("td ellipsis", fieldLength)}" style="${styleCell(fieldLength, fieldName, padding)}">
|
||||
${row[fieldName] || "<span class=\"empty\">-</span>"}
|
||||
<div data-column="${name}" title="${obj[name]}" class="${withCenter("td ellipsis", size)}" style="${styleCell(size, name, padding)}">
|
||||
${obj[name] || "<span class=\"empty\">-</span>"}
|
||||
</div>
|
||||
`));
|
||||
});
|
||||
$dom.tbody.appendChild($tr);
|
||||
}
|
||||
});
|
||||
if (body.length === 0) $dom.tbody.appendChild(createElement(`
|
||||
<h3 class="center no-select" style="opacity:0.2; margin-top:30px">
|
||||
${t("Empty")}
|
||||
</h3>
|
||||
`));
|
||||
transition($dom.tbody.parentElement);
|
||||
}),
|
||||
rxjs.share(),
|
||||
rxjs.catchError(ctrlError()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
all:
|
||||
emcc loader_symbol.c -o loader_symbol.wasm -O2 --no-entry
|
||||
24
public/assets/pages/viewerpage/application_table/loader.js
Normal file
24
public/assets/pages/viewerpage/application_table/loader.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// import loaderDBase from "./loader_dbase.js";
|
||||
// import loaderSymbol from "./loader_symbol.js";
|
||||
|
||||
class ITable {
|
||||
contructor() {}
|
||||
getHeader() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
getBody() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
}
|
||||
|
||||
export async function getLoader(mime) {
|
||||
let module = null;
|
||||
switch (mime) {
|
||||
case "application/dbf":
|
||||
module = await import("./loader_dbase.js");
|
||||
break;
|
||||
case "application/x-archive":
|
||||
module = await import("./loader_symbol.js");
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`unsupported mimetype '${mime}'`);
|
||||
}
|
||||
|
||||
return module.default(ITable);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export default async function(ITable) {
|
||||
const module = await import("../../../lib/vendor/shp-to-geojson.browser.js");
|
||||
|
||||
return class TableImpl extends ITable {
|
||||
constructor(response) {
|
||||
super();
|
||||
this.data = new module.DBase(module.Buffer.from(response));
|
||||
}
|
||||
|
||||
getHeader() {
|
||||
return this.data.properties.map(({ fieldName, fieldLength }) => ({
|
||||
name: fieldName,
|
||||
size: fieldLength,
|
||||
}));
|
||||
}
|
||||
|
||||
getBody() {
|
||||
const body = [];
|
||||
for (let i =0; i<this.data.recordLength; i++) {
|
||||
const row = this.data.getRowProperties(i);
|
||||
body.push(row);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#define ARMAG "!<arch>\n"
|
||||
#define SARMAG sizeof(ARMAG) - 1
|
||||
#define AR_HDR_SIZE 60
|
||||
|
||||
struct ar_hdr {
|
||||
char name[16];
|
||||
char timestamp[12];
|
||||
char owner[6];
|
||||
char group[6];
|
||||
char mode[8];
|
||||
char size[10];
|
||||
char fmag[2];
|
||||
};
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int execute(int fdinput, int fdoutput) {
|
||||
if (fdinput == 0) {
|
||||
fprintf(stderr, "ERROR - missing input %d\n", fdinput);
|
||||
return 1;
|
||||
}
|
||||
if (fdoutput == 0) {
|
||||
fprintf(stderr, "ERROR - missing input %d\n", fdoutput);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE* finput = fdopen(fdinput, "rb");
|
||||
if (!finput) {
|
||||
fprintf(stderr, "ERROR - cannot open input file\n");
|
||||
return 1;
|
||||
}
|
||||
FILE* foutput = fdopen(fdoutput, "wb");
|
||||
if (!foutput) {
|
||||
fprintf(stderr, "ERROR - cannot open output file\n");
|
||||
fclose(finput);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char magic[SARMAG];
|
||||
size_t c = fread(magic, 1, SARMAG, finput);
|
||||
if (c == 0) {
|
||||
fprintf(stderr, "ERROR count=%zu error=%d\n", c, ferror(finput));
|
||||
fclose(finput);
|
||||
fclose(foutput);
|
||||
return 1;
|
||||
}
|
||||
if (strncmp(magic, ARMAG, SARMAG) != 0) {
|
||||
fprintf(stderr, "ERROR file is not of the expected shape");
|
||||
fclose(finput);
|
||||
fclose(foutput);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct ar_hdr header;
|
||||
while (fread(&header, 1, AR_HDR_SIZE, finput) == AR_HDR_SIZE) {
|
||||
if (strncmp(header.fmag, "`\n", 2) != 0) {
|
||||
fprintf(stderr, "Invalid header format.\n");
|
||||
break;
|
||||
}
|
||||
long size = strtol(header.size, NULL, 10);
|
||||
char filename[17] = {0};
|
||||
strncpy(filename, header.name, 16);
|
||||
for (int i = strlen(filename) - 1; i >= 0; i--) {
|
||||
if (filename[i] == ' ' || filename[i] == '/') {
|
||||
filename[i] = '\0';
|
||||
}
|
||||
}
|
||||
if (strlen(filename) > 0) fprintf(
|
||||
foutput,
|
||||
"%.16s,%ld,%ld,%ld,%ld,%ld\n",
|
||||
filename,
|
||||
strtol(header.timestamp, NULL, 10),
|
||||
strtol(header.owner, NULL, 10),
|
||||
strtol(header.group, NULL, 10),
|
||||
strtol(header.mode, NULL, 10),
|
||||
size,
|
||||
);
|
||||
fseek(finput, (size + 1) & ~1, SEEK_CUR);
|
||||
}
|
||||
|
||||
fflush(foutput);
|
||||
fclose(foutput);
|
||||
fclose(finput);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import assert from "../../../lib/assert.js";
|
||||
import loadWASM, { writeFS, readFS } from "../../../helpers/loader_wasm.js";
|
||||
|
||||
export default async function(ITable) {
|
||||
const { instance } = await loadWASM(import.meta.url, "./loader_symbol.wasm");
|
||||
|
||||
return class TableImpl extends ITable {
|
||||
constructor(response) {
|
||||
super();
|
||||
const fdIn = writeFS(new Uint8Array(response));
|
||||
const fdOut = writeFS(new Uint8Array([]));
|
||||
const res = assert.truthy(instance.exports["execute"])(fdIn, fdOut);
|
||||
if (res !== 0) throw new Error(`WASM exited with code=${res}`);
|
||||
const buffer = readFS(fdOut);
|
||||
|
||||
this.header = [
|
||||
{ name: "Mode", size: 3 },
|
||||
{ name: "Timestamp", size: 6 },
|
||||
{ name: "Size", size: 4 },
|
||||
{ name: "Owner", size: 6 },
|
||||
{ name: "Group", size: 6 },
|
||||
{ name: "Object", size: 40 },
|
||||
];
|
||||
this.rows = new TextDecoder().decode(buffer).trimRight().split("\n").map((line) => {
|
||||
const row = line.split(",");
|
||||
return {
|
||||
"Object": row[0],
|
||||
"Timestamp": row[1],
|
||||
"Size": row[5],
|
||||
"Mode": row[4],
|
||||
"Owner": row[2],
|
||||
"Group": row[3],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getHeader() {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
getBody() {
|
||||
return this.rows;
|
||||
}
|
||||
};
|
||||
}
|
||||
BIN
public/assets/pages/viewerpage/application_table/loader_symbol.wasm
Executable file
BIN
public/assets/pages/viewerpage/application_table/loader_symbol.wasm
Executable file
Binary file not shown.
|
|
@ -34,7 +34,7 @@ export function opener(file = "", mimes) {
|
|||
return ["3d", { mime }];
|
||||
} else if (mime === "application/x-url") {
|
||||
return ["url", { mime }];
|
||||
} else if (["application/dbf"].indexOf(mime) !== -1) {
|
||||
} else if (["application/dbf", "application/x-archive"].indexOf(mime) !== -1) {
|
||||
return ["table", { mime }];
|
||||
} else if (type === "application" && mime !== "application/text") {
|
||||
return ["download", { mime }];
|
||||
|
|
|
|||
|
|
@ -130,6 +130,9 @@
|
|||
],
|
||||
"new-cap": [
|
||||
"off"
|
||||
],
|
||||
"accessor-pairs": [
|
||||
"off"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue