Initial commit
This commit is contained in:
commit
3bc7df43f1
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
src/brochures
|
||||
cgid.sock.*
|
||||
apache2-dev.pid
|
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Systeme d'archivage de Brochures
|
||||
|
||||
## Serveur de dev
|
||||
|
||||
```
|
||||
/sbin/apache2 -d "$(pwd)" -f dev.conf -X
|
||||
```
|
14
apache2.conf
Normal file
14
apache2.conf
Normal file
@ -0,0 +1,14 @@
|
||||
DocumentRoot src
|
||||
|
||||
<Directory "src/cgi-bin">
|
||||
Options +ExecCGI
|
||||
AddHandler cgi-script .ts
|
||||
</Directory>
|
||||
|
||||
<Directory "src/brochures">
|
||||
Header set Content-Security-Policy "script-src 'none';"
|
||||
</Directory>
|
||||
|
||||
<Location "/brochures">
|
||||
Script PUT "/cgi-bin/put.ts"
|
||||
</Location>
|
100
dev.conf
Normal file
100
dev.conf
Normal file
@ -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 <VirtualHost>
|
||||
# container, error messages relating to that virtual host will be
|
||||
# logged here. If you *do* define an error logfile for a <VirtualHost>
|
||||
# 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.
|
||||
#
|
||||
<FilesMatch "^\.ht">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
|
||||
#
|
||||
# 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
|
21
src/brochure.template.html
Normal file
21
src/brochure.template.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" vocab="http://schema.org/" resource="." typeof="Book">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>— Archive Infokiosque</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 property="name"></h1>
|
||||
<p property="author"></p>
|
||||
<p>Archivée le <time datetime="" content="" property="uploadDate"></time></p>
|
||||
|
||||
<ul>
|
||||
<li property="encoding" typeof="MediaObject">
|
||||
<a property="contentUrl" href="original.pdf">
|
||||
<span property="name">PDF : Brochure archivée</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
57
src/cgi-bin/put.ts
Executable file
57
src/cgi-bin/put.ts
Executable file
@ -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")
|
||||
}
|
106
src/data.js
Normal file
106
src/data.js
Normal file
@ -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(/--+/, "-")
|
||||
}
|
73
src/index.html
Normal file
73
src/index.html
Normal file
@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Archive Infokiosque</title>
|
||||
<style>
|
||||
body {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.form-control label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Archive Infokiosque <small>Archivez vos brochures</small></h1>
|
||||
|
||||
<noscript>Vous avez besoin d'activer javascript pour archiver quelque chose.</noscript>
|
||||
|
||||
<form id="upload-form" hidden>
|
||||
|
||||
<p class="form-control">
|
||||
<label for="brochure-title">Titre de la brochure</label>
|
||||
<input type="text" name="title" id="brochure-title" required />
|
||||
</p>
|
||||
|
||||
<p class="form-control">
|
||||
<label for="brochure-author">Auteur / Autrice</label>
|
||||
<input type="text" name="author" id="brochure-author" />
|
||||
</p>
|
||||
|
||||
<p class="form-control">
|
||||
<label for="brochure-upload-date">Date d'archivage</label>
|
||||
<input type="date" id="brochure-upload-date" name="upload-date" />
|
||||
</p>
|
||||
|
||||
<p class="form-control">
|
||||
<label for="brochure-file">PDF de la brochure</label>
|
||||
<input type="file" id="brochure-file" accept="application/pdf" name="file" required />
|
||||
</p>
|
||||
|
||||
<button id="submit-button">Archivez moi ca !</button>
|
||||
|
||||
</form>
|
||||
|
||||
<p id="error-container" hidden><output id="error"></output></p>
|
||||
|
||||
<script type="module" src="/index.js"></script>
|
||||
</body>
|
||||
</html>
|
53
src/index.js
Normal file
53
src/index.js
Normal file
@ -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;
|
Loading…
x
Reference in New Issue
Block a user