repo-updater/upload.html
2024-09-19 22:36:34 +08:00

397 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Upload Packages into Repository</title>
<style>
code.code-block {
display: block;
font-family: monospace;
background: black;
color: white;
width: max-content;
padding: 8px 16px 8px 16px;
margin: 8px;
}
table.line-table {
margin: 8px;
}
table.line-table, table.line-table td, th {
border: 1px solid black;
border-collapse: collapse;
padding: 4px;
}
.center-text {
text-align: center;
}
.green-text {
color: lightgreen;
}
</style>
</head>
<body>
<form id="update"><table><tbody>
<tr>
<td>
<label for="arch">Architecture:</label>
</td>
<td>
<select name="arch" id="arch"></select>
</td>
</tr>
<tr>
<td>
<label for="sign">Signature file:</label>
</td>
<td>
<input type="file" name="sign" id="sign" />
</td>
</tr>
<tr>
<td>
<label for="pkg">Package file:</label>
</td>
<td>
<input type="file" name="pkg" id="pkg" />
</td>
</tr>
<tr>
<td>
<label for="folder">Upload folder:</label>
</td>
<td>
<input type="file" name="folder" id="folder" webkitdirectory mozdirectory />
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit" id="add">Add</button>
</td>
</tr>
</tbody></table></form>
<span id="status"></span>
<br/>
<div>
<table id="upload-table" class="line-table">
<thead>
<tr>
<th>Time</th>
<th>Architecture</th>
<th>Name</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody id="upload">
</tbody>
</table>
<button type="button" id="upload-all">Upload all now</button>
</div>
<br/>
<div>
<table id="info-table" class="line-table">
<thead>
<tr>
<th>/</th>
<th>Maximum size</th>
<th>Accept types</th>
</tr>
</thead>
<tbody id="info">
<tr>
<td colspan="3" class="center-text">Loading...</td>
</tr>
</tbody>
</table>
<br/>
<span>Please make sure package is signed correctly before uploading it</span>
<br/>
<span>
You need to confirm that
<code>PACKAGER</code> and <code>GPGKEY</code>
in <code>makepkg.conf</code> are set correctly
</span>
</div>
<br/>
<div>
<span>Upload via shell:</span>
<code class="code-block" id="shell" language="bash">
<span class="green-text"># Upload signature into cache and verify (must sign with allowed key)</span>
<br/>
curl -X PUT --upload-file pkg-0.1-1-any.pkg.tar.xz.sig {SERVER}/pkg-0.1-1-any.pkg.tar.xz.sig
<br/>
<span class="green-text"># Upload package into cache and verify (must match with signature)</span>
<br/>
curl -X PUT --upload-file pkg-0.1-1-any.pkg.tar.xz {SERVER}/pkg-0.1-1-any.pkg.tar.xz
<br/>
<span class="green-text"># Upload into repo and update database</span>
<br/>
curl -X POST -H 'Content-Type: application/json' --data-raw '{"target":"pkg-0.1-1-any.pkg.tar.xz","arch":"aarch64"}' {SERVER}/api/update
<br/>
<span class="green-text"># Cleanup packages cache</span>
<br/>
curl -X DELETE {SERVER}/pkg-0.1-1-any.pkg.tar.xz
<br/>
curl -X DELETE {SERVER}/pkg-0.1-1-any.pkg.tar.xz.sig
</code>
<pre></pre>
</div>
</body>
<script type="text/javascript">
let config={
max_sign_file:0,
max_pkg_file:0,
sign_exts:[],
pkg_exts:[],
arch:[],
};
let uploading={};
let server=window.location.origin;
const form=document.querySelector("form#update");
const arch=document.querySelector("select#arch");
const info=document.querySelector("tbody#info");
const upload=document.querySelector("tbody#upload");
const sign=document.querySelector("input#sign");
const pkg=document.querySelector("input#pkg");
const folder=document.querySelector("input#folder");
const shell=document.querySelector("code#shell");
const add=document.querySelector("button#add");
const upload_all=document.querySelector("button#upload-all");
function setDisabled(disabled){
arch.disabled=disabled;
sign.disabled=disabled;
pkg.disabled=disabled;
add.disabled=disabled;
folder.disabled=disabled;
upload_all.disabled=disabled;
}
function removeChildren(obj){
while(obj.firstChild)
obj.removeChild(obj.firstChild);
}
function setStatus(val){
const status=document.querySelector("span#status");
if(val&&val.length>0){
status.style.display="block";
status.innerText=val;
}else{
status.style.display="block";
status.innerText=val;
}
}
function formatSize(bytes,dp=1){
if(Math.abs(bytes)<1024)return bytes+' B';
const units=['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
let u=-1;
const r=10**dp;
do{
bytes/=1024;u++;
}while(Math.round(Math.abs(bytes)*r)/r>=1024&&u<units.length-1);
return bytes.toFixed(dp)+' '+units[u];
}
function findExt(name,exts){
for(const i in exts)
if(name.endsWith(exts[i]))
return exts[i];
return null;
}
function getFileName(name,exts){
let ext=findExt(name,exts);
if(ext===null)return null;
return name.substring(0,name.length-ext.length);
}
function checkFile(type,obj,max,exts){
if(obj.files.length!==1)throw Error(`Please select ${type} file`);
const file=obj.files[0];
let found=findExt(file.name,exts);
if(found===null)throw Error(`Only ${exts} accepts in ${type}`);
if(file.size>max)throw Error(`File ${type} too big (${file.max}>${max})`);
return true;
}
async function getReason(res){
let reason=""
if(res.headers.get("Content-Type")==="text/plain")
reason=await res.text();
while(reason.endsWith("\r")||reason.endsWith("\n"))
reason=reason.substring(0,reason.length-1);
return reason.length>0?`(${reason})`:"";
}
async function uploadFile(file){
console.log(`uploading ${file.name}`);
const res=await fetch(`${server}/${file.name}`,{
method:'PUT',
body:await file.arrayBuffer()
});
if(res.status!==201)
throw Error(`status not 201: ${res.status} ${await getReason(res)}`);
console.log(`upload ${file.name} done`);
}
async function deleteFile(file){
console.log(`deleting ${file.name}`);
const res=await fetch(`${server}/${file.name}`,{method:'DELETE'});
if(res.status!==200)
throw Error(`status not 200: ${res.status} ${await getReason(res)}`);
console.log(`delete ${file.name} done`);
}
function addColumn(parent=null,text=null){
let col=document.createElement("td");
if(text!==null)col.innerText=text;
if(parent!==null)parent.appendChild(col);
return col;
}
function loadInfo(){
setDisabled(true);
setStatus("Loading...");
fetch(`${server}/api/info`).then(async res=>{
config=await res.json();
removeChildren(arch);
for(const i in config.arch){
const opt=document.createElement("option");
opt.value=config.arch[i];
opt.innerText=config.arch[i];
arch.appendChild(opt);
}
const addInfoRow=(title,size,exts)=>{
let row=document.createElement("tr");
addColumn(row,title);
addColumn(row,formatSize(size));
addColumn(row,exts);
info.appendChild(row);
return row;
}
removeChildren(info);
addInfoRow("Package",config.max_pkg_file,config.pkg_exts);
addInfoRow("Signature",config.max_sign_file,config.sign_exts);
setDisabled(false);
setStatus(null);
}).catch(err=>{
console.error(err);
setDisabled(true);
setStatus(`Load failed: ${err.message}`);
});
}
async function uploadOne(item){
const set_status=val=>{
item.status_obj.innerText=val;
}
try{
set_status(`Uploading signature file`);
await uploadFile(item.sign_file);
set_status(`Uploading package file`);
await uploadFile(item.pkg_file);
set_status(`Updating database`);
const res=await fetch(`${server}/api/update`,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
target:item.pkg_file.name,
arch:item.arch,
})
});
if(res.status!==200)
throw Error(`status not 200: ${res.status} ${await getReason(res)}`);
set_status(`Upload done`);
item.uploaded=true;
}catch(err){
console.error(err);
set_status(`Upload failed: ${err.message}`);
}
const d1=deleteFile(item.sign_file);
const d2=deleteFile(item.pkg_file);
await Promise.all([d1,d2]);
}
function addOne(name){
if(!(name in uploading))return;
let obj=uploading[name];
if(!("sign_file" in obj)||!("pkg_file" in obj))return;
if(!("date" in obj))obj.date=new Date();
if(!("uploaded" in obj))obj.uploaded=false;
if(!("row_obj" in obj)){
obj.row_obj=document.createElement("tr");
obj.date_obj=addColumn(obj.row_obj,obj.date.toLocaleString());
obj.arch_obj=addColumn(obj.row_obj);
obj.name_obj=addColumn(obj.row_obj,name);
obj.size_obj=addColumn(obj.row_obj);
obj.status_obj=addColumn(obj.row_obj,"Waiting");
obj.row_obj.dataset.obj=obj;
upload.appendChild(obj.row_obj);
}
obj.arch_obj.innerText=obj.arch;
obj.size_obj.innerText=formatSize(obj.pkg_file.size);
}
function doAdd(){
try{
if(!arch.value||arch.value.length<=0)throw Error("No architecture selected");
const have_pkg=pkg.files.length===1;
const have_sign=sign.files.length===1;
const have_folder=folder.files.length>=1;
const have_single=have_pkg&&have_sign;
if(have_single){
if(have_pkg&&!have_sign)throw Error("Miss signature");
if(!have_pkg&&have_sign)throw Error("Miss package");
checkFile("signature",sign,config.max_sign_file,config.sign_exts);
checkFile("package",pkg,config.max_pkg_file,config.pkg_exts);
if(sign.files[0].name!==pkg.files[0].name+".sig")
throw Error("Signature mismatch with package file");
const filename=getFileName(pkg.files[0].name,config.pkg_exts);
if(filename===null)throw Error("Bad package filename");
if(!(filename in uploading))uploading[filename]={};
uploading[filename].sign_file=sign.files[0];
uploading[filename].pkg_file=pkg.files[0];
uploading[filename].arch=arch.value;
addOne(filename);
}
if(have_folder)for(let i=0;i<folder.files.length;i++){
let filename;
const file=folder.files[i];
if((filename=getFileName(file.name,config.pkg_exts))!==null){
if(!(filename in uploading))uploading[filename]={};
uploading[filename].pkg_file=file;
uploading[filename].arch=arch.value;
addOne(filename);
}
if((filename=getFileName(file.name,config.sign_exts))!==null){
if(!(filename in uploading))uploading[filename]={};
uploading[filename].sign_file=file;
uploading[filename].arch=arch.value;
addOne(filename);
}
}
if(!have_single&&!have_folder)
throw Error("No any files selected");
setStatus(null);
}catch(err){
console.error(err);
setStatus(`Add failed: ${err.message}`);
}
setDisabled(false);
}
async function doUploadAll(){
setDisabled(true);
setStatus("Uploading");
try{
for(const name in uploading){
const item=uploading[name];
if(!item.uploaded)await uploadOne(item);
}
setStatus("Done");
}catch(err){
console.error(err);
setStatus(`Upload failed: ${err.message}`);
}
setDisabled(false);
}
window.addEventListener("load",()=>{
loadInfo();
shell.innerHTML=shell.innerHTML.replaceAll("{SERVER}",server);
form.onsubmit=()=>{
doAdd();
return false;
};
upload_all.addEventListener("click",()=>doUploadAll());
});
</script>
</html>