397 lines
11 KiB
HTML
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>
|