Initial commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,95 @@
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os, json, uuid, shutil, re
|
||||
from datetime import datetime
|
||||
|
||||
PDF_DIR = "/files"
|
||||
META = "/files/.metadata.json"
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
def load_meta():
|
||||
if os.path.exists(META):
|
||||
with open(META) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_meta(m):
|
||||
with open(META, "w") as f:
|
||||
json.dump(m, f, indent=2)
|
||||
|
||||
def safe_name(name):
|
||||
return re.sub(r'[^\w\-. ]', '_', name)[:200]
|
||||
|
||||
@app.get("/api/list")
|
||||
def list_pdfs():
|
||||
meta = load_meta()
|
||||
items = []
|
||||
for fid, info in meta.items():
|
||||
path = os.path.join(PDF_DIR, info["filename"])
|
||||
if os.path.exists(path):
|
||||
items.append({
|
||||
"id": fid,
|
||||
"title": info.get("title", info["filename"]),
|
||||
"filename": info["filename"],
|
||||
"tag": info.get("tag", ""),
|
||||
"size": os.path.getsize(path),
|
||||
"uploaded": info.get("uploaded", ""),
|
||||
})
|
||||
items.sort(key=lambda x: x["uploaded"], reverse=True)
|
||||
return items
|
||||
|
||||
@app.post("/api/upload")
|
||||
async def upload(file: UploadFile = File(...), title: str = Form(""), tag: str = Form("")):
|
||||
if not file.filename.lower().endswith(".pdf"):
|
||||
raise HTTPException(400, "PDFs only")
|
||||
fid = uuid.uuid4().hex[:12]
|
||||
fname = safe_name(file.filename)
|
||||
path = os.path.join(PDF_DIR, f"{fid}_{fname}")
|
||||
with open(path, "wb") as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
meta = load_meta()
|
||||
meta[fid] = {
|
||||
"filename": f"{fid}_{fname}",
|
||||
"title": title or fname.rsplit(".", 1)[0],
|
||||
"tag": tag,
|
||||
"uploaded": datetime.utcnow().isoformat(),
|
||||
}
|
||||
save_meta(meta)
|
||||
return {"id": fid}
|
||||
|
||||
@app.delete("/api/delete/{fid}")
|
||||
def delete(fid: str):
|
||||
meta = load_meta()
|
||||
if fid not in meta:
|
||||
raise HTTPException(404)
|
||||
path = os.path.join(PDF_DIR, meta[fid]["filename"])
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
del meta[fid]
|
||||
save_meta(meta)
|
||||
return {"ok": True}
|
||||
|
||||
@app.get("/view/{fid}")
|
||||
def view(fid: str):
|
||||
meta = load_meta()
|
||||
if fid not in meta:
|
||||
raise HTTPException(404)
|
||||
path = os.path.join(PDF_DIR, meta[fid]["filename"])
|
||||
return FileResponse(path, media_type="application/pdf")
|
||||
|
||||
@app.patch("/api/update/{fid}")
|
||||
async def update(fid: str, tag: str = Form(None), title: str = Form(None)):
|
||||
meta = load_meta()
|
||||
if fid not in meta:
|
||||
raise HTTPException(404)
|
||||
if tag is not None:
|
||||
meta[fid]["tag"] = tag
|
||||
if title is not None and title.strip():
|
||||
meta[fid]["title"] = title.strip()
|
||||
save_meta(meta)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory="/static", html=True), name="static")
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PDF Library - The Dark Elite</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
:root { --bg:#060708; --panel:#0f1115; --border:rgba(80,90,100,0.2); --accent:#22d3ee; --text:#e5e7eb; --muted:#9ca3af; --danger:#dc2626; }
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;min-height:100vh}
|
||||
.container{max-width:1200px;margin:0 auto;padding:2rem}
|
||||
header{display:flex;justify-content:space-between;align-items:center;padding-bottom:1.5rem;border-bottom:1px solid var(--border);margin-bottom:2rem}
|
||||
header h1{margin:0;font-size:1.5rem;letter-spacing:0.2em;text-transform:uppercase;color:var(--accent);text-shadow:0 0 12px rgba(34,211,238,0.4)}
|
||||
header a{color:var(--muted);text-decoration:none;font-size:0.85rem}
|
||||
header a:hover{color:var(--accent)}
|
||||
.dropzone{border:2px dashed rgba(34,211,238,0.4);border-radius:8px;padding:2rem;text-align:center;background:linear-gradient(135deg,rgba(18,20,24,0.7),rgba(10,11,13,0.85));margin-bottom:2rem;cursor:pointer;transition:all 0.15s}
|
||||
.dropzone:hover,.dropzone.drag{border-color:var(--accent);box-shadow:0 0 20px rgba(34,211,238,0.15)}
|
||||
.dropzone p{margin:0.3rem 0;color:var(--muted);font-size:0.9rem}
|
||||
.dropzone .big{color:var(--accent);font-size:1.1rem;letter-spacing:0.1em;text-transform:uppercase;font-family:ui-monospace,monospace}
|
||||
.dropzone input[type=file]{display:none}
|
||||
.fields{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;margin-top:1rem}
|
||||
.fields input{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.5rem 0.8rem;border-radius:4px;font-size:0.85rem}
|
||||
.fields input:focus{outline:none;border-color:var(--accent)}
|
||||
.section-title{font-size:0.75rem;letter-spacing:0.25em;text-transform:uppercase;color:var(--accent);border-left:3px solid var(--accent);padding-left:0.75rem;margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center}
|
||||
.search{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.4rem 0.7rem;border-radius:4px;font-size:0.8rem;font-family:ui-monospace,monospace}
|
||||
.search:focus{outline:none;border-color:var(--accent)}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem}
|
||||
.card{background:linear-gradient(135deg,rgba(18,20,24,0.9),rgba(10,11,13,0.95));border:1px solid var(--border);border-radius:6px;padding:1rem;transition:all 0.15s;position:relative;overflow:hidden}
|
||||
.card:hover{border-color:rgba(34,211,238,0.5);transform:translateY(-2px);box-shadow:0 0 25px rgba(34,211,238,0.18)}
|
||||
.card .title{font-size:0.95rem;color:var(--text);margin-bottom:0.3rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-weight:500}
|
||||
.card .meta{font-size:0.7rem;color:var(--muted);font-family:ui-monospace,monospace;letter-spacing:0.05em}
|
||||
.card .tag{display:inline-block;background:rgba(34,211,238,0.12);border:1px solid rgba(34,211,238,0.3);color:rgb(165,235,247);font-size:0.65rem;padding:0.15rem 0.5rem;border-radius:3px;margin-top:0.5rem;font-family:ui-monospace,monospace;text-transform:uppercase;letter-spacing:0.1em}
|
||||
.card .actions{display:flex;gap:0.4rem;margin-top:0.7rem}
|
||||
.card .actions a,.card .actions button{flex:1;background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.35rem;border-radius:3px;font-size:0.7rem;cursor:pointer;text-align:center;text-decoration:none;letter-spacing:0.1em;text-transform:uppercase}
|
||||
.card .actions a:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.card .actions button:hover{border-color:var(--danger);color:var(--danger)}
|
||||
.empty{color:var(--muted);text-align:center;padding:2rem;font-style:italic}
|
||||
.toast{position:fixed;bottom:2rem;right:2rem;background:rgba(18,20,24,0.95);border:1px solid var(--accent);color:var(--accent);padding:0.8rem 1.2rem;border-radius:4px;font-family:ui-monospace,monospace;font-size:0.85rem;z-index:100;display:none}
|
||||
|
||||
.tag-row{margin-top:0.5rem;min-height:1.4rem}
|
||||
.tag-edit{display:inline-block;background:rgba(34,211,238,0.12);border:1px solid rgba(34,211,238,0.3);color:rgb(165,235,247);font-size:0.65rem;padding:0.15rem 0.5rem;border-radius:3px;font-family:ui-monospace,monospace;text-transform:uppercase;letter-spacing:0.1em;cursor:pointer;transition:all 0.15s}
|
||||
.tag-edit:empty::before,.tag-edit:hover{border-color:rgba(34,211,238,0.7);color:var(--accent)}
|
||||
.tag-edit.empty{background:transparent;border-style:dashed;color:var(--muted)}
|
||||
.tag-edit input{background:rgba(10,11,13,0.9);border:1px solid var(--accent);color:var(--accent);font:inherit;padding:0;width:8rem;outline:none;text-transform:uppercase}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div><h1>PDF Library</h1></div>
|
||||
<a href="/">← Hub</a>
|
||||
</header>
|
||||
|
||||
<div class="dropzone" id="dz">
|
||||
<p class="big">Drop PDFs here or click to upload</p>
|
||||
<p>Supports drag-drop, multiple files</p>
|
||||
<input type="file" id="fileInput" accept=".pdf" multiple>
|
||||
<div class="fields">
|
||||
<input type="text" id="titleInput" placeholder="Title (optional - uses filename)">
|
||||
<input type="text" id="tagInput" placeholder="Tag (medical, repair, etc.)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">
|
||||
Library
|
||||
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||
<select class="search" id="sortInput" style="font-family:ui-monospace,monospace">
|
||||
<option value="date-desc">Newest First</option>
|
||||
<option value="date-asc">Oldest First</option>
|
||||
<option value="title-asc">Title A-Z</option>
|
||||
<option value="title-desc">Title Z-A</option>
|
||||
<option value="size-desc">Size (Largest)</option>
|
||||
<option value="size-asc">Size (Smallest)</option>
|
||||
<option value="tag-asc">Tag A-Z</option>
|
||||
</select>
|
||||
<input type="text" class="search" id="searchInput" placeholder="Filter...">
|
||||
</div>
|
||||
</div>
|
||||
<div id="grid" class="grid"><div class="empty">Loading...</div></div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script>
|
||||
let allItems = [];
|
||||
|
||||
function fmtSize(b){
|
||||
if(b<1024) return b+" B";
|
||||
if(b<1048576) return (b/1024).toFixed(1)+" KB";
|
||||
if(b<1073741824) return (b/1048576).toFixed(1)+" MB";
|
||||
return (b/1073741824).toFixed(2)+" GB";
|
||||
}
|
||||
function fmtDate(s){
|
||||
if(!s) return "";
|
||||
return new Date(s).toLocaleDateString();
|
||||
}
|
||||
|
||||
function toast(msg){
|
||||
const t=document.getElementById("toast");
|
||||
t.textContent=msg;
|
||||
t.style.display="block";
|
||||
setTimeout(()=>t.style.display="none",2500);
|
||||
}
|
||||
|
||||
async function loadList(){
|
||||
const r=await fetch("api/list");
|
||||
allItems=await r.json();
|
||||
render();
|
||||
}
|
||||
|
||||
function render(){
|
||||
const q=document.getElementById("searchInput").value.toLowerCase();
|
||||
const sort=document.getElementById("sortInput").value;
|
||||
let items=allItems.filter(i=>!q || i.title.toLowerCase().includes(q) || (i.tag||"").toLowerCase().includes(q));
|
||||
items.sort((a,b)=>{
|
||||
switch(sort){
|
||||
case "date-desc": return (b.uploaded||"").localeCompare(a.uploaded||"");
|
||||
case "date-asc": return (a.uploaded||"").localeCompare(b.uploaded||"");
|
||||
case "title-asc": return a.title.localeCompare(b.title);
|
||||
case "title-desc":return b.title.localeCompare(a.title);
|
||||
case "size-desc": return b.size-a.size;
|
||||
case "size-asc": return a.size-b.size;
|
||||
case "tag-asc": return (a.tag||"").localeCompare(b.tag||"") || a.title.localeCompare(b.title);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const grid=document.getElementById("grid");
|
||||
if(!items.length){
|
||||
grid.innerHTML='<div class="empty">No PDFs yet. Upload some above.</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML=items.map(i=>`
|
||||
<div class="card">
|
||||
<div class="title">${i.title}</div>
|
||||
<div class="meta">${fmtSize(i.size)} • ${fmtDate(i.uploaded)}</div>
|
||||
<div class="tag-row"><span class="tag-edit ${i.tag?'':'empty'}" onclick="editTag('${i.id}', this)">${i.tag||"+ add tag"}</span></div>
|
||||
<div class="actions">
|
||||
<a href="view/${i.id}" target="_blank">View</a>
|
||||
<button onclick="del('${i.id}')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
async function del(fid){
|
||||
if(!confirm("Delete this PDF?")) return;
|
||||
await fetch("api/delete/"+fid,{method:"DELETE"});
|
||||
toast("Deleted");
|
||||
loadList();
|
||||
}
|
||||
|
||||
async function upload(files){
|
||||
for(const file of files){
|
||||
if(!file.name.toLowerCase().endsWith(".pdf")){ toast("Skipped: "+file.name); continue; }
|
||||
const fd=new FormData();
|
||||
fd.append("file",file);
|
||||
fd.append("title",document.getElementById("titleInput").value);
|
||||
fd.append("tag",document.getElementById("tagInput").value);
|
||||
toast("Uploading "+file.name+"...");
|
||||
const r=await fetch("api/upload",{method:"POST",body:fd});
|
||||
if(r.ok) toast("Uploaded "+file.name);
|
||||
else toast("Failed: "+file.name);
|
||||
}
|
||||
document.getElementById("titleInput").value="";
|
||||
document.getElementById("tagInput").value="";
|
||||
loadList();
|
||||
}
|
||||
|
||||
const dz=document.getElementById("dz");
|
||||
const fi=document.getElementById("fileInput");
|
||||
dz.addEventListener("click",e=>{ if(e.target.tagName!=="INPUT") fi.click(); });
|
||||
fi.addEventListener("change",e=>upload(e.target.files));
|
||||
dz.addEventListener("dragover",e=>{ e.preventDefault(); dz.classList.add("drag"); });
|
||||
dz.addEventListener("dragleave",()=>dz.classList.remove("drag"));
|
||||
dz.addEventListener("drop",e=>{ e.preventDefault(); dz.classList.remove("drag"); upload(e.dataTransfer.files); });
|
||||
document.getElementById("searchInput").addEventListener("input",render);
|
||||
document.getElementById("sortInput").addEventListener("change",render);
|
||||
|
||||
|
||||
async function editTag(fid, el){
|
||||
if(el.querySelector("input")) return;
|
||||
const current = el.textContent === "+ add tag" ? "" : el.textContent;
|
||||
el.innerHTML = '<input type="text" value="'+current+'" />';
|
||||
const input = el.querySelector("input");
|
||||
input.focus();
|
||||
input.select();
|
||||
const save = async () => {
|
||||
const val = input.value.trim();
|
||||
const fd = new FormData();
|
||||
fd.append("tag", val);
|
||||
await fetch("api/update/"+fid, {method:"PATCH", body: fd});
|
||||
toast(val ? "Tag set: "+val : "Tag cleared");
|
||||
loadList();
|
||||
};
|
||||
input.addEventListener("blur", save);
|
||||
input.addEventListener("keydown", e => {
|
||||
if(e.key === "Enter") input.blur();
|
||||
if(e.key === "Escape"){ input.value = current; input.blur(); }
|
||||
});
|
||||
}
|
||||
|
||||
loadList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, json, uuid, re
|
||||
from datetime import datetime
|
||||
|
||||
PDF_DIR = "/opt/hub/pdfs/files"
|
||||
META = os.path.join(PDF_DIR, ".metadata.json")
|
||||
|
||||
TAG_RULES = [
|
||||
(r'fm[-_ ]?\d|atp[-_ ]?\d|tm[-_ ]?\d|tc[-_ ]?\d|army|military|combat|tactical|sniper|infantry|guerrilla|special.forces', 'defense'),
|
||||
(r'medic|first.aid|surgery|trauma|nursing|hesperian|midwif|dentist|disease|emergency.med|tccc|where.there.is.no.doctor', 'medical'),
|
||||
(r'survival|prepper|shtf|bug.out|wilderness|evasion', 'survival'),
|
||||
(r'radio|ham|antenna|arrl|fcc|comms|aprs|signal', 'comms'),
|
||||
(r'food|cook|cann|preserv|recipe|kitchen|baking|ferment', 'food'),
|
||||
(r'farm|garden|crop|seed|livestock|chicken|poultry|cattle|sheep|goat|beekeep|honey|agriculture|permaculture', 'agriculture'),
|
||||
(r'vet|animal|dog|cat|horse|pig|pet', 'veterinary'),
|
||||
(r'navig|map|compass|gps|bowditch|topo', 'navigation'),
|
||||
(r'solar|wind|battery|grid|power|nrel|energy|generator', 'energy'),
|
||||
(r'electric|plumb|carpent|hvac|welding|machinist|trade|repair|audel', 'trades'),
|
||||
(r'chem|biolog|physic|crc.handbook|merck.index', 'chemistry'),
|
||||
(r'engineer|mechanic|machine|handbook', 'engineering'),
|
||||
(r'law|legal|constitution|statute|code|court|right', 'law'),
|
||||
(r'finance|tax|irs|money|invest|insurance', 'finance'),
|
||||
(r'history|war|civil|world.war|geopolit', 'history'),
|
||||
]
|
||||
|
||||
def infer_tag(filename):
|
||||
name = filename.lower()
|
||||
for pattern, tag in TAG_RULES:
|
||||
if re.search(pattern, name):
|
||||
return tag
|
||||
return 'general'
|
||||
|
||||
def clean_title(filename):
|
||||
name = filename.rsplit('.', 1)[0]
|
||||
name = re.sub(r'[_-]+', ' ', name).strip()
|
||||
return name[:200]
|
||||
|
||||
def main():
|
||||
if os.path.exists(META):
|
||||
with open(META) as f:
|
||||
meta = json.load(f)
|
||||
else:
|
||||
meta = {}
|
||||
|
||||
registered = {info['filename'] for info in meta.values()}
|
||||
added = 0
|
||||
skipped = 0
|
||||
for fname in sorted(os.listdir(PDF_DIR)):
|
||||
if not fname.lower().endswith('.pdf'): continue
|
||||
if fname.startswith('.'): continue
|
||||
if fname in registered:
|
||||
skipped += 1
|
||||
continue
|
||||
path = os.path.join(PDF_DIR, fname)
|
||||
if os.path.getsize(path) < 50000: continue
|
||||
fid = uuid.uuid4().hex[:12]
|
||||
meta[fid] = {
|
||||
'filename': fname,
|
||||
'title': clean_title(fname),
|
||||
'tag': infer_tag(fname),
|
||||
'uploaded': datetime.utcnow().isoformat(),
|
||||
}
|
||||
added += 1
|
||||
|
||||
with open(META, 'w') as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
|
||||
print(f"Added: {added} Already registered: {skipped} Total in library: {len(meta)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
# COMPREHENSIVE Reference PDF Library Seeder
|
||||
# Covers Trades, Agriculture, Energy, Wilderness, Food, Chemistry,
|
||||
# Mechanical, Comms, Medical, Military, Gov/Law, Navigation, Skills,
|
||||
# Finance, Pet/Livestock
|
||||
set -u
|
||||
TMP=/tmp/pdf-seed-all
|
||||
API=http://localhost/pdfs/api/upload
|
||||
mkdir -p "$TMP"
|
||||
|
||||
PDFS=(
|
||||
# ========== TRADES & REPAIR ==========
|
||||
"https://archive.org/download/bwb_Y0-BMG-117/bwb_Y0-BMG-117.pdf|Audels Plumbers and Steam Fitters Guide #3 (1925)|trades"
|
||||
"https://archive.org/download/cu31924004933945/cu31924004933945.pdf|Henleys Twentieth Century Book of Formulas (1909)|trades"
|
||||
"https://archive.org/download/theboymechanicvo12655gut/theboymechanicvo12655gut.pdf|The Boy Mechanic Vol 1 (1913)|trades"
|
||||
"https://archive.org/download/the-boy-mechanic-book-2-1000-things-for-unknown/The%20Boy%20Mechanic%2C%20Book%202%20%281915%29%20-%201%2C000%20Things%20for%20Boys%20to%20Do.pdf|The Boy Mechanic Vol 2 (1915)|trades"
|
||||
"https://rexresearch1.com/AudelManuals/AudelPipefittersWeldersPocketManual.pdf|Audel Pipefitters and Welders Pocket Manual|trades"
|
||||
|
||||
# ========== AGRICULTURE / SELF-SUFFICIENCY ==========
|
||||
# USDA Farmers Bulletins must be sourced individually; placing 1 sample
|
||||
"https://naldc.nal.usda.gov/download/CAT74420061/PDF|USDA Farmers Bulletin No 1186 - Pork on the Farm|agriculture"
|
||||
|
||||
# ========== ENERGY / OFF-GRID ==========
|
||||
"https://www.nrel.gov/docs/fy20osti/76011.pdf|NREL Off-Grid PV System Design Sizing Guide|energy"
|
||||
|
||||
# ========== WILDERNESS / OUTDOOR ==========
|
||||
# Boy Scout Handbook 1911 is text-only on Gutenberg; sourcing alternate scan
|
||||
"https://archive.org/download/boyscoutshandboo00boys/boyscoutshandboo00boys.pdf|Boy Scouts Handbook First Edition (1911)|wilderness"
|
||||
|
||||
# ========== FOOD / COOKING / PRESERVATION ==========
|
||||
"https://archive.org/download/bostoncookingsc00farm/bostoncookingsc00farm.pdf|Boston Cooking School Cook Book - Fannie Farmer (1918)|food"
|
||||
"https://nchfp.uga.edu/how/dry/food_drying_what_you_need_to_know.pdf|USDA NCHFP - Food Drying What You Need to Know|food"
|
||||
|
||||
# ========== CHEMISTRY / SCIENCE ==========
|
||||
"https://archive.org/download/CRCHandbookOfChemistryAndPhysics97thEdition2016/CRC%20Handbook%20of%20Chemistry%20and%20Physics%2C%2097th%20Edition.pdf|CRC Handbook of Chemistry and Physics 97th Edition (2016)|chemistry"
|
||||
|
||||
# ========== MECHANICAL / ENGINEERING ==========
|
||||
# Machinery's Handbook 6th already added in original batch
|
||||
|
||||
# ========== VEHICLE (manual sourcing recommended) ==========
|
||||
# Ram 1500 / Cub Cadet FSMs typically need OEM login - skipped
|
||||
|
||||
# ========== COMMUNICATIONS ==========
|
||||
# ARRL/Antenna books are paid; skipped
|
||||
|
||||
# ========== MEDICAL DEPTH ==========
|
||||
"https://ia801609.us.archive.org/9/items/civilian-and-non-civilian-medical-guides/Ranger%20Medic%20Handbook%20%282007%29.pdf|Ranger Medic Handbook (2007)|medical"
|
||||
"https://archive.org/download/SOFMH2001/SOFMH2001.pdf|Special Operations Forces Medical Handbook (2001)|medical"
|
||||
"https://archive.org/download/where-there-is-no-vet/where-there-is-no-vet.pdf|Where There Is No Vet|veterinary"
|
||||
|
||||
# ========== MILITARY / TACTICAL ==========
|
||||
"https://archive.org/download/Jungle_Operations_FM_90-5/Jungle_Operations_FM_90-5.pdf|FM 90-5 Jungle Operations (1982)|defense"
|
||||
"https://ia801302.us.archive.org/16/items/FM31_21_1961/FM31_21_1961.pdf|FM 31-21 Guerrilla Warfare and Special Forces Operations (1961)|defense"
|
||||
"https://archive.org/download/usa-tm-31-210-improvised-munitions-handbook/USA%20TM%2031%20210%20Improvised%20Munitions%20Handbook.pdf|TM 31-210 Improvised Munitions Handbook (1969)|defense"
|
||||
|
||||
# ========== GOVERNMENT / LAW ==========
|
||||
"https://archive.org/download/TheFederalistPapers_201607/The%20Federalist%20Papers.pdf|The Federalist Papers|law"
|
||||
"https://archive.org/download/roberts-rules-of-order/roberts-rules-of-order.pdf|Roberts Rules of Order (Parliamentary Procedure)|law"
|
||||
"https://statutes.capitol.texas.gov/docs/sdocs/penalcode.pdf|Texas Penal Code|law"
|
||||
"https://statutes.capitol.texas.gov/docs/sdocs/familycode.pdf|Texas Family Code|law"
|
||||
"https://statutes.capitol.texas.gov/docs/sdocs/propertycode.pdf|Texas Property Code|law"
|
||||
"https://statutes.capitol.texas.gov/docs/sdocs/cn.pdf|Texas Constitution|law"
|
||||
|
||||
# ========== NAVIGATION DEPTH ==========
|
||||
# Bowditch V1+V2 already included; USGS topo symbols below
|
||||
"https://pubs.usgs.gov/gip/TopographicMapSymbols/topomapsymbols.pdf|USGS Topographic Map Symbols|navigation"
|
||||
|
||||
# ========== SPECIFIC SKILLS ==========
|
||||
# Welding/knots/blacksmithing - most under restricted access, manual sourcing
|
||||
|
||||
# ========== FINANCE / PRACTICAL LIFE ==========
|
||||
"https://www.irs.gov/pub/irs-pdf/p17.pdf|IRS Publication 17 - Your Federal Income Tax|finance"
|
||||
|
||||
# ========== PETS / LIVESTOCK ==========
|
||||
# Where There Is No Vet already in medical above
|
||||
)
|
||||
|
||||
ok=0; fail=0; total=${#PDFS[@]}; i=0
|
||||
for entry in "${PDFS[@]}"; do
|
||||
i=$((i+1))
|
||||
IFS='|' read -r url title tag <<< "$entry"
|
||||
fname=$(echo "$title" | tr -c 'A-Za-z0-9._-' '_' | head -c 100).pdf
|
||||
out="$TMP/$fname"
|
||||
echo ""
|
||||
echo "==> [$i/$total] [$title]"
|
||||
if wget -q --show-progress -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" --tries=2 --timeout=120 -O "$out" "$url"; then
|
||||
sz_bytes=$(stat -c%s "$out" 2>/dev/null || echo 0)
|
||||
if [ "$sz_bytes" -gt 100000 ] && file "$out" | grep -qi "PDF"; then
|
||||
sz=$(du -h "$out" | cut -f1)
|
||||
curl -s -X POST "$API" -F "file=@$out;type=application/pdf" -F "title=$title" -F "tag=$tag" > /dev/null
|
||||
echo " ✓ uploaded ($sz)"
|
||||
ok=$((ok+1))
|
||||
else
|
||||
echo " ✗ skipped (not a valid PDF, $sz_bytes bytes)"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
rm -f "$out"
|
||||
else
|
||||
echo " ✗ download failed"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "OK: $ok Failed: $fail Total: $total"
|
||||
echo "================================"
|
||||
rm -rf "$TMP"
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
TMP=/tmp/pdf-seed-fix
|
||||
API=http://localhost/pdfs/api/upload
|
||||
mkdir -p "$TMP"
|
||||
|
||||
PDFS=(
|
||||
"https://ia801301.us.archive.org/5/items/WhereWomenHaveNoDoctor/18.HersperianFoundation-WhereWomenHaveNoDoctor.pdf|Where Women Have No Doctor (Hesperian)|medical"
|
||||
"https://archive.org/download/WhereThereIsNoDoctor-English-DavidWerner/WhereThereIsNoDoctor-English-DavidWerner.pdf|Where There Is No Doctor (Hesperian)|medical"
|
||||
"https://ia801500.us.archive.org/28/items/MManuals/Fm5-31Boobytraps.pdf|FM 5-31 Boobytraps (1965)|defense"
|
||||
"https://archive.org/download/milmanual-fm-3-25.26-map-reading-and-land-navigation/fm_3-25.26_map_reading_and_land_navigation.pdf|FM 3-25.26 Map Reading and Land Navigation|navigation"
|
||||
"https://archive.org/download/milmanual-fm-23-10-sniper-training/fm_23-10_sniper_training.pdf|FM 23-10 Sniper Training|defense"
|
||||
"https://archive.org/download/milmanual-fm-21-150-combatives/fm_21-150_combatives.pdf|FM 21-150 Combatives|defense"
|
||||
"https://archive.org/download/milmanual-fm-3-06.11-combined-arms-operations-in-urban-terrain/fm_3-06.11_combined_arms_operations_in_urban_terrain.pdf|FM 3-06.11 Combined Arms Urban Terrain|defense"
|
||||
"https://armypubs.army.mil/epubs/DR_pubs/DR_a/ARN36119-TC_3-22.9-000-WEB-1.pdf|TC 3-22.9 Rifle and Carbine Marksmanship (v2)|defense"
|
||||
)
|
||||
|
||||
ok=0; fail=0; total=${#PDFS[@]}; i=0
|
||||
for entry in "${PDFS[@]}"; do
|
||||
i=$((i+1))
|
||||
IFS='|' read -r url title tag <<< "$entry"
|
||||
fname=$(echo "$title" | tr -c 'A-Za-z0-9._-' '_' | head -c 100).pdf
|
||||
out="$TMP/$fname"
|
||||
echo ""
|
||||
echo "==> [$i/$total] [$title]"
|
||||
if wget -q --show-progress -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" --tries=2 --timeout=60 -O "$out" "$url"; then
|
||||
sz_bytes=$(stat -c%s "$out" 2>/dev/null || echo 0)
|
||||
if [ "$sz_bytes" -gt 100000 ] && file "$out" | grep -qi "PDF"; then
|
||||
sz=$(du -h "$out" | cut -f1)
|
||||
curl -s -X POST "$API" -F "file=@$out;type=application/pdf" -F "title=$title" -F "tag=$tag" > /dev/null
|
||||
echo " ✓ uploaded ($sz)"
|
||||
ok=$((ok+1))
|
||||
else
|
||||
echo " ✗ skipped (not a valid PDF, $sz_bytes bytes)"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
rm -f "$out"
|
||||
else
|
||||
echo " ✗ download failed"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "OK: $ok Failed: $fail Total: $total"
|
||||
echo "================================"
|
||||
rm -rf "$TMP"
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
TMP=/tmp/pdf-seed-fix2
|
||||
API=http://localhost/pdfs/api/upload
|
||||
mkdir -p "$TMP"
|
||||
|
||||
PDFS=(
|
||||
"https://archive.org/download/fm21-150-combatives/fm21-150-combatives.pdf|FM 21-150 Combatives (1992)|defense"
|
||||
"https://archive.org/download/WhereThereIsNoDoctor-English-DavidWerner/WhereThereIsNoDoctor-English-DavidWerner.pdf|Where There Is No Doctor (Hesperian)|medical"
|
||||
"https://www.globalsecurity.org/military/library/policy/army/fm/3-22-9/fm3-22-9_c1_2011.pdf|FM 3-22.9 Rifle Marksmanship M16/M4 (2011)|defense"
|
||||
)
|
||||
|
||||
ok=0; fail=0; total=${#PDFS[@]}; i=0
|
||||
for entry in "${PDFS[@]}"; do
|
||||
i=$((i+1))
|
||||
IFS='|' read -r url title tag <<< "$entry"
|
||||
fname=$(echo "$title" | tr -c 'A-Za-z0-9._-' '_' | head -c 100).pdf
|
||||
out="$TMP/$fname"
|
||||
echo ""
|
||||
echo "==> [$i/$total] [$title]"
|
||||
if wget -q --show-progress -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" --tries=2 --timeout=120 -O "$out" "$url"; then
|
||||
sz_bytes=$(stat -c%s "$out" 2>/dev/null || echo 0)
|
||||
if [ "$sz_bytes" -gt 100000 ] && file "$out" | grep -qi "PDF"; then
|
||||
sz=$(du -h "$out" | cut -f1)
|
||||
curl -s -X POST "$API" -F "file=@$out;type=application/pdf" -F "title=$title" -F "tag=$tag" > /dev/null
|
||||
echo " ✓ uploaded ($sz)"
|
||||
ok=$((ok+1))
|
||||
else
|
||||
echo " ✗ skipped (not a valid PDF, $sz_bytes bytes)"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
rm -f "$out"
|
||||
else
|
||||
echo " ✗ download failed"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "OK: $ok Failed: $fail Total: $total"
|
||||
echo "================================"
|
||||
rm -rf "$TMP"
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# Reference PDF Library - Full SHTF/Prep Collection
|
||||
set -u
|
||||
TMP=/tmp/pdf-seed
|
||||
API=http://localhost/pdfs/api/upload
|
||||
mkdir -p "$TMP"
|
||||
|
||||
PDFS=(
|
||||
# ========== MEDICAL - HESPERIAN (full books from archive.org mirror) ==========
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_where_there_is_no_doctor_2017_full_book.pdf|Where There Is No Doctor (Hesperian 2017)|medical"
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_where_there_is_no_dentist_2012_full_book.pdf|Where There Is No Dentist (Hesperian)|medical"
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_a_book_for_the_midwives_2013_full_book.pdf|A Book for Midwives (Hesperian)|medical"
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_disabled_village_children_2009_full_book.pdf|Disabled Village Children (Hesperian)|medical"
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_helping_children_who_are_deaf_2004_full_book.pdf|Helping Children Who Are Deaf (Hesperian)|medical"
|
||||
"https://archive.org/download/strategic_intelligence_network/medical/hesperian_-_where_women_have_no_doctor_full_book.pdf|Where Women Have No Doctor (Hesperian)|medical"
|
||||
|
||||
# ========== MEDICAL - MILITARY / TACTICAL ==========
|
||||
"https://tccc.org.ua/files/downloads/clinical-guidelines-2024-en.pdf|TCCC Guidelines 2024 (Tactical Combat Casualty Care)|medical"
|
||||
"https://archive.org/download/Fm4-25.11FirstAid/Fm4-25.11FirstAid.pdf|FM 4-25.11 First Aid|medical"
|
||||
"https://archive.org/download/FM21-10_2000/FM21-10_2000.pdf|FM 21-10 Field Hygiene and Sanitation (2000)|medical"
|
||||
|
||||
# ========== SURVIVAL ==========
|
||||
"https://trueprepper.com/wp-content/uploads/2022/11/FM-21-76-US-Army-Survival-Manual.pdf|FM 21-76 US Army Survival Manual (1992)|survival"
|
||||
"https://archive.org/download/MManuals/Fm3-05.70Survival.pdf|FM 3-05.70 Survival (2002)|survival"
|
||||
"https://www.globalsecurity.org/military/library/policy/army/fm/21-76-1/fm_21-76-1survival.pdf|FM 21-76-1 Survival Evasion Recovery|survival"
|
||||
|
||||
# ========== MILITARY / DEFENSE ==========
|
||||
"https://armypubs.army.mil/epubs/DR_pubs/DR_a/pdf/web/ARN36119_TC%203-22x9%20FINAL%20WEB.pdf|TC 3-22.9 Rifle and Carbine Marksmanship|defense"
|
||||
"https://armypubs.army.mil/epubs/DR_pubs/DR_a/pdf/web/ARN20697_FM%203-06%20FINAL%20WEB%201.pdf|FM 3-06 Urban Operations|defense"
|
||||
"https://armypubs.army.mil/epubs/DR_pubs/DR_a/pdf/web/atp3_21x8.pdf|ATP 3-21.8 Infantry Platoon and Squad|defense"
|
||||
"https://www.bits.de/NRANEU/others/amd-us-archive/fm5-31%2865%29.pdf|FM 5-31 Boobytraps|defense"
|
||||
|
||||
# ========== NAVIGATION ==========
|
||||
"https://www.globalsecurity.org/military/library/policy/army/fm/3-25-26/fm3-25-26.pdf|FM 3-25.26 Map Reading and Land Navigation|navigation"
|
||||
"https://navlist.net/imgx/Bowditch-2017-v1.pdf|Bowditch American Practical Navigator V1 (2017)|navigation"
|
||||
"https://navlist.net/imgx/Bowditch-2017-v2.pdf|Bowditch American Practical Navigator V2 (2017)|navigation"
|
||||
|
||||
# ========== COMMUNICATIONS ==========
|
||||
"https://www.cisa.gov/sites/default/files/2024-12/NIFOG%202.02_508%20FINAL%20VERSION%2012%2003%202024.pdf|NIFOG 2.02 - National Interop Field Ops Guide|comms"
|
||||
"https://www.qsl.net/w/wb4bxw/books/ARRL_Ham_Radio_License_Manual.pdf|ARRL Ham Radio License Manual (Technician)|comms"
|
||||
"https://www.govinfo.gov/content/pkg/CFR-2024-title47-vol5/pdf/CFR-2024-title47-vol5-part97.pdf|FCC Part 97 Amateur Radio Service Rules (2024)|comms"
|
||||
|
||||
# ========== PREPAREDNESS ==========
|
||||
"https://www.ready.gov/sites/default/files/2021-11/are-you-ready-guide.pdf|FEMA Are You Ready Guide|preparedness"
|
||||
|
||||
# ========== FOOD PRESERVATION ==========
|
||||
"https://archive.org/download/usda-complete-guide-to-home-canning-2015-revision/USDA-Complete-Guide-to-Home-Canning-2015-revision.pdf|USDA Complete Guide to Home Canning 2015|food"
|
||||
|
||||
# ========== TRADES / REPAIR (Audel, vintage public domain) ==========
|
||||
"https://archive.org/download/audel.carpenter.no1.1923/audel.carpenter.no1.1923.pdf|Audels Carpenters and Builders Guide Vol 1 (1923)|repair"
|
||||
|
||||
# ========== ENGINEERING / SHOP ==========
|
||||
"https://archive.org/download/machineryshandbo00indu/machineryshandbo00indu.pdf|Machinery's Handbook 6th Edition (1924)|engineering"
|
||||
)
|
||||
|
||||
ok=0; fail=0; total=${#PDFS[@]}
|
||||
i=0
|
||||
for entry in "${PDFS[@]}"; do
|
||||
i=$((i+1))
|
||||
IFS='|' read -r url title tag <<< "$entry"
|
||||
fname=$(echo "$title" | tr -c 'A-Za-z0-9._-' '_' | head -c 100).pdf
|
||||
out="$TMP/$fname"
|
||||
echo ""
|
||||
echo "==> [$i/$total] [$title]"
|
||||
echo " URL: $url"
|
||||
if wget -q --show-progress -U "Mozilla/5.0" --tries=2 --timeout=60 -O "$out" "$url"; then
|
||||
if [ -s "$out" ] && file "$out" | grep -qi "PDF"; then
|
||||
sz=$(du -h "$out" | cut -f1)
|
||||
curl -s -X POST "$API" -F "file=@$out;type=application/pdf" -F "title=$title" -F "tag=$tag" > /dev/null
|
||||
echo " ✓ uploaded ($sz)"
|
||||
ok=$((ok+1))
|
||||
else
|
||||
echo " ✗ skipped (not a valid PDF)"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
rm -f "$out"
|
||||
else
|
||||
echo " ✗ download failed"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "OK: $ok Failed: $fail Total: $total"
|
||||
echo "================================"
|
||||
rm -rf "$TMP"
|
||||
Reference in New Issue
Block a user