From 3bc7df43f13025d857fc2f58426370efec630c02 Mon Sep 17 00:00:00 2001 From: EpicKiwi Date: Sun, 12 Jan 2025 00:55:41 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ README.md | 7 +++ apache2.conf | 14 +++++ dev.conf | 100 ++++++++++++++++++++++++++++++++++ src/brochure.template.html | 21 ++++++++ src/cgi-bin/put.ts | 57 ++++++++++++++++++++ src/data.js | 106 +++++++++++++++++++++++++++++++++++++ src/index.html | 73 +++++++++++++++++++++++++ src/index.js | 53 +++++++++++++++++++ 9 files changed, 434 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apache2.conf create mode 100644 dev.conf create mode 100644 src/brochure.template.html create mode 100755 src/cgi-bin/put.ts create mode 100644 src/data.js create mode 100644 src/index.html create mode 100644 src/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a33cfd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +src/brochures +cgid.sock.* +apache2-dev.pid \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0460691 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Systeme d'archivage de Brochures + +## Serveur de dev + +``` +/sbin/apache2 -d "$(pwd)" -f dev.conf -X +``` \ No newline at end of file diff --git a/apache2.conf b/apache2.conf new file mode 100644 index 0000000..d424878 --- /dev/null +++ b/apache2.conf @@ -0,0 +1,14 @@ +DocumentRoot src + + + Options +ExecCGI + AddHandler cgi-script .ts + + + + Header set Content-Security-Policy "script-src 'none';" + + + + Script PUT "/cgi-bin/put.ts" + \ No newline at end of file diff --git a/dev.conf b/dev.conf new file mode 100644 index 0000000..b1f7c28 --- /dev/null +++ b/dev.conf @@ -0,0 +1,100 @@ +PidFile "apache2-dev.pid" +DefaultRuntimeDir "." + +# +# Timeout: The number of seconds before receives and sends time out. +# +Timeout 300 + +# +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +# +KeepAlive On + +# +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +# +MaxKeepAliveRequests 100 + +# +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +# +KeepAliveTimeout 5 + +# +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +# +HostnameLookups Off + +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog /dev/stderr + +# +# LogLevel: Control the severity of messages logged to the error_log. +# Available values: trace8, ..., trace1, debug, info, notice, warn, +# error, crit, alert, emerg. +# It is also possible to configure the log level for particular modules, e.g. +# "LogLevel info ssl:warn" +# +LogLevel debug + +# Include module configuration: +IncludeOptional /etc/apache2/mods-enabled/*.load +IncludeOptional /etc/apache2/mods-enabled/*.conf + +# Include list of ports to listen on +Listen 8080 + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +# +AccessFileName .htaccess + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + + +# +# The following directives define some format nicknames for use with +# a CustomLog directive. +# +# These deviate from the Common Log Format definitions in that they use %O +# (the actual bytes sent including headers) instead of %b (the size of the +# requested file), because the latter makes it impossible to detect partial +# requests. +# +# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended. +# Use mod_remoteip instead. +# +LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined +LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %O" common +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +# Include of directories ignores editors' and dpkg's backup files, +# see README.Debian for details. + +ScriptSock cgid.sock + +Include apache2.conf diff --git a/src/brochure.template.html b/src/brochure.template.html new file mode 100644 index 0000000..66d5749 --- /dev/null +++ b/src/brochure.template.html @@ -0,0 +1,21 @@ + + + + + + — Archive Infokiosque + + +

+

+

Archivée le

+ + + + \ No newline at end of file diff --git a/src/cgi-bin/put.ts b/src/cgi-bin/put.ts new file mode 100755 index 0000000..c06a221 --- /dev/null +++ b/src/cgi-bin/put.ts @@ -0,0 +1,57 @@ +#!/bin/env -S deno run --allow-env=PATH_TRANSLATED,REQUEST_METHOD --allow-write=../brochures/ + +import * as path from "jsr:@std/path"; + +async function fail(status){ + for await(let byte of Deno.stdin.readable){ + + } + console.log("Status: "+status) + console.log("Content-Type: text/plain") + console.log() +} + +let targetPath = Deno.env.get("PATH_TRANSLATED") + +if(!targetPath){ + await fail(400) + Deno.exit(0) +} + +if(Deno.env.get("REQUEST_METHOD") != "PUT"){ + await fail(400) + Deno.exit(0) +} + +await Deno.mkdir(path.dirname(targetPath), { + recursive: true, + mode: 0o770 +}) + +let targetFile = null; + +try { + targetFile = await Deno.open(targetPath, { + write: true, + createNew: true, + mode: 0o660 + }) +} catch(e) { + if(e.name == "AlreadyExists"){ + await fail(409) + Deno.exit(0) + } else { + console.error(e) + throw e + } +} + + +if(targetFile){ + await Deno.stdin.readable.pipeTo(targetFile.writable) + + console.log("Status: 201") + console.log("Content-Type: text/plain") + console.log() + console.log("Created") +} \ No newline at end of file diff --git a/src/data.js b/src/data.js new file mode 100644 index 0000000..f5a1ae5 --- /dev/null +++ b/src/data.js @@ -0,0 +1,106 @@ +const DATA_URL = new URL("/brochures/", document.baseURI) +const PARSER = new DOMParser() +const SERIALIZER = new XMLSerializer() +const DATE_FORMATTER = new Intl.DateTimeFormat("fr", {dateStyle: "short"}) + +export async function uploadDocument(documentFile, documentDetails = {}){ + + let uniqueName = await getUniqueName(documentDetails) + + let baseURI = new URL(`./${uniqueName}/`, DATA_URL) + + let originalFileName = `${slugify(documentDetails.title)}-original.pdf` + + let uploadProgress = fetch(new URL(`./${originalFileName}`, baseURI), { + method: "PUT", + headers: { + "Content-Type": "application/pdf" + }, + body: documentFile + }) + + let page = PARSER.parseFromString(await (await fetch("/brochure.template.html")).text(), "text/html") + + page.title = documentDetails.title + " " + page.title + + page.querySelector('[property="name"]').textContent = documentDetails.title + page.querySelector('a[href="original.pdf"]').href = "./"+originalFileName + + { + let el = page.querySelector('[property="uploadDate"]') + el.textContent = DATE_FORMATTER.format(new Date(documentDetails.uploadDate)) + el.setAttribute("datetime", documentDetails.uploadDate); + el.setAttribute("content", documentDetails.uploadDate); + } + + if(documentDetails.author){ + page.querySelector('[property="author"]').textContent = documentDetails.author + } else { + page.querySelector('[property="author"]').remove() + } + + let responses = await Promise.all([ + fetch(new URL("./index.html", baseURI), { + method: "PUT", + headers: { + "Content-Type": "text/html" + }, + body: SERIALIZER.serializeToString(page) + }), + uploadProgress + ]); + + for(let res of responses) { + if(!res.ok){ + throw new Error(`Network error ${res.status} ${res.statusText}`) + } + } + + return baseURI.toString() +} + +async function getUniqueName(documentDetails){ + let nameCandidate, isUnique; + + do { + nameCandidate = getRandomName(documentDetails) + + let res = await fetch(new URL(`./${nameCandidate}/index.html`, DATA_URL), { + method: "HEAD" + }); + + if(!res.ok && res.status != 404){ + throw new Error(`Network error ${res.status} ${res.statusText}`) + } + + isUnique = res.status == 404 + + } while(!isUnique) + + + return nameCandidate +} + +function getRandomName(documentDetails){ + let source = new Uint8Array(16) + crypto.getRandomValues(source) + let parts = [toHex(source)] + + if(documentDetails.author){ + parts.push(slugify(documentDetails.author)) + } + + if(documentDetails.title){ + parts.push(slugify(documentDetails.title)) + } + + return parts.join("-") +} + +function toHex(buffer) { + return Array.prototype.map.call(buffer, x => x.toString(16).padStart(2, '0')).join(''); +} + +function slugify(str){ + return str.toLowerCase().replace(/\//g,"-").replace(/\s/g, "-").replace(/--+/, "-") +} \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..fa3199a --- /dev/null +++ b/src/index.html @@ -0,0 +1,73 @@ + + + + + + Archive Infokiosque + + + +

Archive Infokiosque Archivez vos brochures

+ + + + + + + + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fc1e62d --- /dev/null +++ b/src/index.js @@ -0,0 +1,53 @@ +import * as db from "/data.js" + +let form = document.getElementById("upload-form") +let submitButton = document.getElementById("submit-button") +let errorContainer = document.getElementById("error-container") +let errorEl = document.getElementById("error") + +form.addEventListener("submit", async e => { + e.preventDefault() + submitButton.disabled = true + errorContainer.hidden = true + let data = new FormData(e.target); + + try { + + if(!data.get("title")){ + throw new Error("Veuillez donner le titre de la brochure") + } + + if(!data.get("file")){ + throw new Error("Aucun fichier selectionne") + } + + let details = { + title: data.get("title") + } + + if(data.get("author")){ + details.author = data.get("author") + } + + if(data.get("upload-date")){ + details.uploadDate = data.get("upload-date") + } + + document.location = await db.uploadDocument(data.get("file"), details) + + } catch(e){ + errorEl.textContent = `Erreur : ${e.message}` + errorContainer.hidden = false + console.error(e) + } + + submitButton.disabled = false +}) + +// INIT + +console.log() + +let now = new Date(); +form.elements["upload-date"].value = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, "0")}-${(now.getDate()).toString().padStart(2, "0")}` +form.hidden = false; \ No newline at end of file