diff --git a/README.md b/README.md index 02a2706..18348b8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ sudo ./scripts/download-content.sh **6. Maps.** Open `http:///admin` (user `iiab-admin`), go to the maps section of the **Install Content** menu and choose the regions you want. +**Alternative, or if that download is blocked (e.g. in Turkey):** `sudo ./scripts/download-map.sh` cuts a vector map of Turkey (about 1.7 GB, down to zoom 15) from the [Protomaps](https://protomaps.com) planet build and installs a small map page. It only talks to `build.protomaps.com`, `github.com` and `cdn.jsdelivr.net`, and nginx serves the files as they are, so there is no extra server config. Open **http://box.lan/harita/**. It works next to the IIAB map, it does not replace it. Another region: `sudo MAP_NAME=germany MAP_BBOX=5.8,47.2,15.1,55.1 ./scripts/download-map.sh` (bbox is `west,south,east,north`). `--update` re-cuts the map when a newer planet build exists. Map data © OpenStreetMap contributors (ODbL). + **7. Use it.** Join the Wi-Fi network you created and open **http://box.lan/home**. ### Optional: internet over a USB Wi-Fi adapter @@ -87,15 +89,30 @@ Every 6 hours (and 5 minutes after boot) `scripts/disk-health.sh` checks the `/l Check now: `sudo ./scripts/disk-health.sh`. Disks behind some USB adapters don't report SMART; those checks are then skipped silently. +### Automatic updates + +`install.sh` sets this up (on an existing install: `sudo ./scripts/install-auto-update.sh`). When the Pi gets an internet connection (Ethernet, the USB Wi-Fi adapter above, a phone hotspot), it checks which content and map have a newer version. It **never downloads on its own**: the homepage shows the list with sizes; pick items, enter the `iiab-admin` password (the one set during install, also used for `/admin`) and press **Seçilenleri güncelle**. + +- On a phone hotspot or another metered connection the homepage warns that the download uses mobile data. +- If an item does not fit on the disk it cannot be selected; the old version keeps working. +- The check runs at most every 6 hours, plus once a day for boxes that stay online. +- Check now: `sudo ./scripts/auto-update.sh --check --force`. Update from the terminal: `sudo ./scripts/auto-update.sh --apply wikipedia_tr_all_maxi`. Logs: `journalctl -u has-update-check -u has-update-apply` + +**From a USB drive, without internet.** Copy `.zim` files (with their original names, e.g. `wikipedia_tr_all_maxi_2026-09.zim`) to a USB drive, anywhere up to three folders deep, and plug it into the Pi. The drive is mounted read-only and the files listed in `config/content.conf` that are missing or newer than what the box has show up in the same list, marked "(USB'den)". Before a copied file is used, the MD5 checksum stored inside every ZIM file is verified, so a damaged copy is never installed. The checksum does not prove who made the file, so only approve drives from a source you trust. The SSD that holds `/library` is never treated as a content drive, even when it is connected over USB. Use exFAT or NTFS for files over 4 GB. + ### Repo layout ``` install.sh main installer scripts/setup-ssd.sh mounts the SSD at /library +scripts/download-map.sh offline vector map (Protomaps) and its page scripts/download-content.sh downloads/updates ZIM files from Kiwix scripts/install-homepage.sh installs the custom homepage +scripts/auto-update.sh checks for and applies updates +scripts/install-auto-update.sh installs the update services scripts/status.sh health check: disk, services, hotspot, content, power scripts/disk-health.sh disk and power health check (saglik/: its systemd timer) +update/ update services, NetworkManager hook, USB rule, nginx config config/iiab-overrides.yml Internet-in-a-Box settings config/content.conf list of content to download www/home/index.html homepage @@ -113,7 +130,7 @@ It checks free space on the SD card and SSD, the web, Kiwix, hotspot and DHCP se - **`ssh user@hostname.local` stops working after install.** Internet-in-a-Box renames the Pi to `box`. Use `ssh user@box.local` or the Pi's IP address. To find the IP, run `arp -a` on your computer or check your router's device list. - **Chromium says the profile is locked.** This also comes from the hostname change. Click "Unlock Profile", or run `rm -f ~/.config/chromium/Singleton*`. - **The hotspot doesn't appear.** Connect over Ethernet, check `sudo iiab-diagnostics`, and make sure the install finished: `/etc/iiab/install-flags/iiab-complete` must exist. -- **Map downloads fail with "connection reset by peer".** The map server (`contabostorage.com`) is blocked in some countries, e.g. Turkey. Install Cloudflare WARP on the Pi, connect, retry the download in the admin console, then disconnect: +- **Map downloads fail with "connection reset by peer".** The map server (`contabostorage.com`) is blocked in some countries, e.g. Turkey. The easy way out is `sudo ./scripts/download-map.sh` (step 6), which does not use that server. Otherwise install Cloudflare WARP on the Pi, connect, retry the download in the admin console, then disconnect: ```bash curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(. /etc/os-release; echo $VERSION_CODENAME) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list @@ -197,6 +214,8 @@ sudo ./scripts/download-content.sh **6. Haritalar.** `http:///admin` adresine gir (kullanıcı adı `iiab-admin`), **Install Content** menüsündeki harita bölümüne git ve istediğin bölgeleri seç. +**Alternatif, ya da o indirme engelliyse (ör. Türkiye'de):** `sudo ./scripts/download-map.sh` [Protomaps](https://protomaps.com) planet build'inden Türkiye'nin vektör haritasını (yaklaşık 1,7 GB, 15. yakınlaştırma düzeyine kadar) keser ve küçük bir harita sayfası kurar. Yalnızca `build.protomaps.com`, `github.com` ve `cdn.jsdelivr.net` adreslerine gider; dosyaları nginx olduğu gibi sunar, ek sunucu ayarı gerekmez. **http://box.lan/harita/** adresini aç. IIAB haritasının yanında çalışır, onun yerine geçmez. Başka bölge: `sudo MAP_NAME=almanya MAP_BBOX=5.8,47.2,15.1,55.1 ./scripts/download-map.sh` (bbox sırası `batı,güney,doğu,kuzey`). `--update` daha yeni bir planet build'i varsa haritayı yeniden keser. Harita verisi © OpenStreetMap katkıcıları (ODbL). + **7. Kullan.** Oluşturduğun Wi-Fi ağına bağlan ve **http://box.lan/home** adresini aç. ### İsteğe bağlı: USB Wi-Fi adaptörüyle internet @@ -225,15 +244,30 @@ Pi'nin dahili Wi-Fi kartı sadece hotspot için kullanılıyor. Pi'nin geçici o Hemen kontrol: `sudo ./scripts/disk-health.sh`. Bazı USB adaptörlerinin arkasındaki diskler SMART bilgisi vermez; o zaman bu kontrol sessizce atlanır. +### Otomatik güncelleme + +`install.sh` bunu kurar (mevcut kurulumda: `sudo ./scripts/install-auto-update.sh`). Pi internete bağlandığında (Ethernet, yukarıdaki USB Wi-Fi adaptörü, telefondan paylaşım) hangi içeriğin ve haritanın yeni sürümü olduğuna bakar. **Kendiliğinden hiçbir şey indirmez:** ana sayfada boyutlarıyla bir liste çıkar; içerikleri seçip `iiab-admin` şifresini (kurulumda belirlenen, `/admin` girişinde de kullanılan şifre) girerek **Seçilenleri güncelle** butonuna basılır. + +- Telefondan paylaşım gibi kotalı bir bağlantıdaysa ana sayfa indirmenin mobil veri harcayacağını söyler. +- Diske sığmayan içerik seçilemez; eski sürüm çalışmaya devam eder. +- Kontrol en fazla 6 saatte bir yapılır, sürekli bağlı cihazlarda ayrıca günde bir. +- Hemen kontrol: `sudo ./scripts/auto-update.sh --check --force`. Terminalden güncelleme: `sudo ./scripts/auto-update.sh --apply wikipedia_tr_all_maxi`. Kayıtlar: `journalctl -u has-update-check -u has-update-apply` + +**İnternetsiz, USB diskten.** `.zim` dosyalarını orijinal adlarıyla (ör. `wikipedia_tr_all_maxi_2026-09.zim`) bir USB diske, en fazla üç klasör derinliğe kopyala ve Pi'ye tak. Disk salt okunur bağlanır; `config/content.conf`'ta olup kutuda olmayan ya da kutudakinden yeni olan dosyalar aynı listede "(USB'den)" etiketiyle çıkar. Kopyalanan dosya kullanılmadan önce her ZIM dosyasının içinde saklanan MD5 özeti doğrulanır, bozuk kopya asla kurulmaz. Bu özet dosyayı kimin hazırladığını kanıtlamaz; yalnızca güvendiğin bir kaynaktan gelen diski onayla. `/library`'yi tutan SSD, USB ile bağlı olsa bile içerik diski sayılmaz. 4 GB'tan büyük dosyalar için diski exFAT ya da NTFS biçimlendir. + ### Repo yapısı ``` install.sh ana kurulum betiği scripts/setup-ssd.sh SSD'yi /library klasörüne bağlar +scripts/download-map.sh çevrimdışı vektör harita (Protomaps) ve sayfası scripts/download-content.sh Kiwix'ten ZIM dosyalarını indirir/günceller scripts/install-homepage.sh özel ana sayfayı kurar +scripts/auto-update.sh güncellemeleri kontrol eder ve uygular +scripts/install-auto-update.sh güncelleme servislerini kurar scripts/status.sh sağlık kontrolü: disk, servisler, hotspot, içerik, güç scripts/disk-health.sh disk ve güç sağlığı denetimi (saglik/: systemd zamanlayıcısı) +update/ güncelleme servisleri, NetworkManager kancası, USB kuralı, nginx ayarı config/iiab-overrides.yml Internet-in-a-Box ayarları config/content.conf indirilecek içerik listesi www/home/index.html ana sayfa @@ -251,7 +285,7 @@ SD kart ve SSD'deki boş alanı, web, Kiwix, hotspot ve DHCP servislerini, hotsp - **Kurulumdan sonra `ssh kullanici@hostname.local` çalışmıyor.** Internet-in-a-Box cihazın adını `box` yapar. `ssh kullanici@box.local` ya da Pi'nin IP adresini kullan. IP adresini bulmak için bilgisayarında `arp -a` çalıştır ya da modeminin cihaz listesine bak. - **Chromium "profile is locked" hatası veriyor.** Bu da cihaz adının değişmesinden kaynaklanır. "Unlock Profile" butonuna tıkla ya da `rm -f ~/.config/chromium/Singleton*` komutunu çalıştır. - **Hotspot görünmüyor.** Ethernet ile bağlan, `sudo iiab-diagnostics` komutuyla kontrol et ve kurulumun bittiğinden emin ol: `/etc/iiab/install-flags/iiab-complete` dosyası var olmalı. -- **Harita indirmesi "connection reset by peer" hatası veriyor.** Harita sunucusu (`contabostorage.com`) Türkiye gibi bazı ülkelerde engelli. Pi'ye Cloudflare WARP kur, bağlan, indirmeyi yönetim panelinden tekrar başlat ve bitince bağlantıyı kapat: +- **Harita indirmesi "connection reset by peer" hatası veriyor.** Harita sunucusu (`contabostorage.com`) Türkiye gibi bazı ülkelerde engelli. Kolay çıkış yolu `sudo ./scripts/download-map.sh` (adım 6): o sunucuyu kullanmaz. Aksi halde Pi'ye Cloudflare WARP kur, bağlan, indirmeyi yönetim panelinden tekrar başlat ve bitince bağlantıyı kapat: ```bash curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(. /etc/os-release; echo $VERSION_CODENAME) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list diff --git a/install.sh b/install.sh index 6d6947a..7360ab4 100755 --- a/install.sh +++ b/install.sh @@ -76,9 +76,10 @@ else fi # -------------------------------------------------------------- 3. ana sayfa -say "3/3 Ana sayfa / homepage" +say "3/3 Ana sayfa ve otomatik güncelleme / homepage and automatic updates" "$REPO/scripts/install-homepage.sh" "$REPO/scripts/install-disk-health.sh" +"$REPO/scripts/install-auto-update.sh" IP=$(hostname -I | awk '{print $1}') say "Bitti / done" diff --git a/scripts/auto-update.sh b/scripts/auto-update.sh new file mode 100755 index 0000000..1821c48 --- /dev/null +++ b/scripts/auto-update.sh @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# Otomatik güncelleme. Kendiliğinden hiçbir şey indirmez: +# --check internet varsa yeni sürümleri bulur, ana sayfanın okuduğu updates.json'a yazar +# --apply ana sayfada onaylanan içerikleri indirir, ilerlemeyi update-status.json'a yazar +# --usb takılı USB disklerdeki daha yeni ZIM'leri bulur, usb.json'a yazar (internet gerekmez) +# Automatic updates. Never downloads anything on its own: +# --check if online, finds newer versions and writes them to updates.json (read by the homepage) +# --apply downloads the items approved on the homepage, progress goes to update-status.json +# --usb finds newer ZIMs on plugged-in USB drives and writes them to usb.json (no internet needed) +# İndirmeyi download-content.sh ve download-map.sh yapar (SHA-256, yer kontrolü, eski sürümü silme). +# The actual downloads are done by download-content.sh and download-map.sh. +# +# Kullanım / usage: +# sudo ./scripts/auto-update.sh --check [--force] --force olmadan 6 saatte birden sık bakmaz / at most every 6 h without --force +# sudo ./scripts/auto-update.sh --apply [id ...] id yoksa ana sayfanın istek dosyasını okur / without ids reads the homepage request +# sudo ./scripts/auto-update.sh --usb [--usb-add|--usb-remove sdX1] has-usb@.service çağırır / called by has-usb@.service +# Ortam / environment: WEB=/library/www/html DEST=/library/zims/content RESERVE_GB=5 CHECK_HOURS=6 +set -uo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +CONF="$REPO/config/content.conf" +WEB="${WEB:-/library/www/html}" +DEST="${DEST:-/library/zims/content}" +RESERVE_GB="${RESERVE_GB:-5}" +CHECK_HOURS="${CHECK_HOURS:-6}" +STATE_DIR="${STATE_DIR:-/var/lib/has-update}" +REQUEST="${REQUEST:-/run/has-update/request}" +LOCK="${LOCK:-/run/lock/has-update.lock}" +UPDATES="$WEB/home/updates.json" +STATUS="$WEB/home/update-status.json" +MAP_META="$WEB/harita/data/map.json" +# usb.json /run altında durur, yeniden başlatınca eski liste kalmaz / lives in /run so a reboot clears a stale list +USB_DIR="${USB_DIR:-/run/has-usb}" +USB_JSON="$USB_DIR/usb.json" +USB_ROOTS="${USB_ROOTS:-$USB_DIR /media}" +BUILDS_URL=https://build-metadata.protomaps.dev/builds.json +BUILD_BASE=https://build.protomaps.com +GB=$((1024 * 1024 * 1024)) + +mode=""; FORCE=0; ids=(); usbact=""; usbdev="" +for a in "$@"; do + case "$a" in + --check|--apply|--usb) mode=$a;; + --usb-add|--usb-remove) mode=--usb; usbact=$a;; + --force) FORCE=1;; + -*) echo "bilinmeyen seçenek / unknown option: $a" >&2; exit 2;; + *) if [ -n "$usbact" ]; then usbdev=$a; else ids+=("$a"); fi;; + esac +done +[ -n "$mode" ] || { echo "kullanım / usage: $0 --check [--force] | --apply [id ...] | --usb" >&2; exit 2; } +[ "$EUID" -eq 0 ] || { echo "sudo ile çalıştır / run with sudo" >&2; exit 1; } + +# JSON'a yalnızca bu karakterlerden oluşan değerler yazılır, kaçış gerekmez. +# Only values made of these characters reach the JSON, so no escaping is needed. +safe(){ [[ "$1" =~ ^[A-Za-z0-9._-]+$ ]]; } +write_json(){ cat > "$1.tmp" && chmod 644 "$1.tmp" && mv "$1.tmp" "$1"; } +field(){ sed -nE "s/.*\"$1\":\"?([^,\"}]*).*/\1/p" <<<"$2"; } + +online(){ + local i + for i in 1 2 3; do + curl -fsSI -m 20 -o /dev/null https://download.kiwix.org/zim/ && return 0 + sleep 20 + done + return 1 +} + +# Telefon paylaşımı gibi kotalı bağlantıları NetworkManager "metered" olarak işaretler. +# NetworkManager marks connections such as phone hotspots as metered. +metered(){ + local dev; dev=$(ip route show default 2>/dev/null | awk '{print $5; exit}') + if [ -n "$dev" ] && nmcli -t -g GENERAL.METERED device show "$dev" 2>/dev/null | grep -q '^yes'; then echo true; else echo false; fi +} + +check(){ + if [ "$FORCE" = 0 ] && [ -f "$STATE_DIR/last-check" ] \ + && [ $(( $(date +%s) - $(stat -c %Y "$STATE_DIR/last-check") )) -lt $((CHECK_HOURS * 3600)) ]; then + echo "== son kontrol $CHECK_HOURS saatten yeni, atlandı / checked within $CHECK_HOURS h, skipped"; return 0 + fi + online || { echo "== internet yok, kontrol atlandı / no internet, check skipped"; return 0; } + echo ">> yeni sürümler aranıyor / looking for newer versions" + + local avail items="" sep="" folder prefix latest size current fits + avail=$(df -B1 --output=avail "$DEST" | tail -1 | tr -dc '0-9') + while read -r _ folder prefix latest size; do + safe "$folder" && safe "$prefix" && safe "$latest" || continue + current=$(cd "$DEST" && ls "$prefix"_[0-9][0-9][0-9][0-9]-[0-9][0-9]*.zim 2>/dev/null | tail -1) + [[ "$size" =~ ^[0-9]+$ ]] || size=null + fits=true; [ "$size" != null ] && [ "$avail" -lt $((size + RESERVE_GB * GB)) ] && fits=false + items+="$sep"$'\n'" {\"id\":\"$prefix\",\"kind\":\"zim\",\"folder\":\"$folder\",\"current\":\"$current\",\"latest\":\"$latest\",\"size\":$size,\"fits\":$fits}" + sep="," + done < <(CHECK_ONLY=1 DEST="$DEST" "$REPO/scripts/download-content.sh" "$CONF" | grep '^UPDATE ') + + # Harita yalnızca kuruluysa ve daha yeni planet build'i varsa / map only if installed and a newer build exists + if [ -f "$MAP_META" ]; then + local name have build bytes mapavail i d + name=$(sed -nE 's/.*"name": *"([^"]*)".*/\1/p' "$MAP_META") + have=$(sed -nE 's/.*"build": *"([0-9]{8})".*/\1/p' "$MAP_META") + bytes=$(sed -nE 's/.*"bytes": *([0-9]+).*/\1/p' "$MAP_META") + build=$(curl -fsSL -m 60 "$BUILDS_URL" | grep -oE '"key":"[0-9]{8}\.pmtiles"' | grep -oE '[0-9]{8}' | sort | tail -1) + # builds.json bazı bağlantılardan açılmıyor; o zaman son günlerin dosyası doğrudan sorulur. + # builds.json is unreachable from some connections; then ask for recent daily files directly. + if [ -z "$build" ]; then + for i in $(seq 0 10); do + d=$(date -u -d "-$i day" +%Y%m%d) + curl -fsI -m 20 -o /dev/null "$BUILD_BASE/$d.pmtiles" && { build=$d; break; } + done + fi + if safe "$name" && [ -n "$have" ] && [ -n "$build" ] && [ "$build" -gt "$have" ]; then + # download-map.sh ile aynı kural / same rule as download-map.sh: kesit ~3 GB + pay / extract ~3 GB + headroom + mapavail=$(df -B1 --output=avail "$WEB" | tail -1 | tr -dc '0-9') + fits=true; [ "$mapavail" -lt $(((RESERVE_GB + 3) * GB)) ] && fits=false + items+="$sep"$'\n'" {\"id\":\"harita\",\"kind\":\"map\",\"name\":\"$name\",\"current\":\"$have\",\"latest\":\"$build\",\"size\":${bytes:-null},\"fits\":$fits}" + fi + fi + + write_json "$UPDATES" < $UPDATES" +} + +# USB diski salt okunur bağlar. fstab'daki ya da zaten bağlı diskler (ör. USB'li SSD) atlanır. +# Mounts a USB drive read-only. Drives in fstab or already mounted (e.g. a USB SSD) are skipped. +usb_add(){ + local dev="$1" uuid + safe "$dev" && [ -b "/dev/$dev" ] || { echo "!! geçersiz aygıt / invalid device: $dev" >&2; return 1; } + if findmnt -rn -S "/dev/$dev" >/dev/null; then echo "== /dev/$dev zaten bağlı, atlandı / already mounted, skipped"; return 0; fi + uuid=$(blkid -s UUID -o value "/dev/$dev") + if [ -n "$uuid" ] && grep -q "UUID=$uuid" /etc/fstab; then echo "== /dev/$dev fstab'da, atlandı / in fstab, skipped"; return 0; fi + mkdir -p "$USB_DIR/$dev" + mount -o ro,nosuid,nodev,noexec "/dev/$dev" "$USB_DIR/$dev" || { rmdir "$USB_DIR/$dev"; return 1; } + echo "OK /dev/$dev -> $USB_DIR/$dev" +} + +usb_remove(){ + safe "$1" || return 1 + if mountpoint -q "$USB_DIR/$1"; then umount -l "$USB_DIR/$1"; fi + rmdir "$USB_DIR/$1" 2>/dev/null || true +} + +usb_scan(){ + local avail items="" sep="" seen=" " base prefix current src size fits conf + avail=$(df -B1 --output=avail "$DEST" | tail -1 | tr -dc '0-9') + # MD5 bozulmayı yakalar, değiştirilmiş dosyayı yakalamaz; USB'den yalnızca content.conf'taki içerikler alınır. + # The MD5 catches damage, not tampering; only content listed in content.conf is taken from a USB drive. + conf=" $(awk '$1 !~ /^#/ && NF >= 2 {printf "%s ", $2}' "$CONF")" + # Ters sıralı: aynı ZIM'in birden çok sürümü varsa en yenisi önce gelir / reverse order: newest version first + while read -r base; do + [[ "$base" =~ ^([A-Za-z0-9._-]+)_[0-9]{4}-[0-9]{2}[a-z]?\.zim$ ]] || continue + prefix=${BASH_REMATCH[1]} + case "$conf" in *" $prefix "*) ;; *) continue;; esac + case "$seen" in *" $prefix "*) continue;; esac + seen+="$prefix " + current=$(cd "$DEST" && ls "$prefix"_[0-9][0-9][0-9][0-9]-[0-9][0-9]*.zim 2>/dev/null | tail -1) + [ -n "$current" ] && [[ ! "$base" > "$current" ]] && continue + src=$(find $USB_ROOTS -maxdepth 5 -type f -name "$base" -print -quit 2>/dev/null) + size=$(stat -c %s "$src") + fits=true; [ "$avail" -lt $((size + RESERVE_GB * GB)) ] && fits=false + items+="$sep"$'\n'" {\"id\":\"usb.$prefix\",\"kind\":\"usb\",\"file\":\"$base\",\"current\":\"$current\",\"latest\":\"$base\",\"size\":$size,\"fits\":$fits}" + sep="," + done < <(find $USB_ROOTS -maxdepth 5 -type f -name '*.zim' -printf '%f\n' 2>/dev/null | sort -ru) + mkdir -p "$USB_DIR" + write_json "$USB_JSON" < $USB_JSON" +} + +# ZIM dosyasının son 16 baytı, öncesinin MD5 özetidir (openZIM biçimi; yeri başlıkta checksumPos). +# The last 16 bytes of a ZIM file are the MD5 of everything before them (openZIM format, checksumPos in the header). +zim_ok(){ + python3 - "$1" <<'EOF' +import hashlib, struct, sys +with open(sys.argv[1], "rb") as f: + head = f.read(80) + if len(head) < 80 or head[:4] != b"ZIM\x04": + sys.exit(1) + left = struct.unpack_from("/dev/null) + [ -n "$src" ] || { echo "!! $file USB'de bulunamadı / not found on the USB drive"; return 1; } + avail=$(df -B1 --output=avail "$DEST" | tail -1 | tr -dc '0-9') + if [ "$avail" -lt $(( $(stat -c %s "$src") + RESERVE_GB * GB )) ]; then + echo "!! $file için yer yok / not enough space"; return 1 + fi + cp "$src" "$DEST/$file.part" || { echo "!! kopyalanamadı / copy failed"; rm -f "$DEST/$file.part"; return 1; } + echo " ZIM sağlaması doğrulanıyor / verifying ZIM checksum" + zim_ok "$DEST/$file.part" || { echo "!! $file bozuk, silindi / corrupt, deleted"; rm -f "$DEST/$file.part"; return 1; } + mv "$DEST/$file.part" "$DEST/$file" + for old in "$DEST/$prefix"_[0-9][0-9][0-9][0-9]-[0-9][0-9]*.zim; do + [ "$old" != "$DEST/$file" ] && [ -f "$old" ] && { echo "-- eski sürüm siliniyor / removing $old"; rm -f "$old"; } + done + iiab-make-kiwix-lib +} + +results="" +status(){ # durum [ek alanlar] / state [extra fields] + write_json "$STATUS" < "$conf" + DEST="$DEST" "$REPO/scripts/download-content.sh" "$conf" & + fi + pid=$! + while kill -0 "$pid" 2>/dev/null; do + # .part bitince yeniden adlandırılır; o zaman son dosyanın boyutu okunur / after the rename read the final file + got=$(stat -c %s "$part" "${part%.part}" 2>/dev/null | head -1) + status running "\"current\":{\"id\":\"$id\",\"done\":${got:-0},\"size\":$size},\"queue\":[$queue]" + sleep 3 + done + wait "$pid"; local rc=$? + [ -n "$conf" ] && rm -f "$conf" + return $rc +} + +apply(){ + if [ ${#ids[@]} -eq 0 ] && [ -f "$REQUEST" ]; then mapfile -t ids < "$REQUEST"; fi + rm -f "$REQUEST" + [ ${#ids[@]} -gt 0 ] || { echo "== güncellenecek bir şey seçilmedi / nothing selected"; return 0; } + + local i j id line queue failed=0 ok + for i in "${!ids[@]}"; do + id=${ids[$i]} + safe "$id" || { echo "!! geçersiz ad / invalid id: $id" >&2; failed=1; continue; } + queue=""; for j in "${ids[@]:i+1}"; do safe "$j" && queue+="${queue:+,}\"$j\""; done + line=$(grep -shF "{\"id\":\"$id\"," "$UPDATES" "$USB_JSON" | head -1) + if [ -z "$line" ] || [[ "$line" != *'"fits":true'* ]]; then + echo "!! $id güncelleme listesinde yok ya da yer yok / not in the update list or no space" >&2; ok=false + else + echo ">> $id güncelleniyor / updating" + if run_item "$id" "$line" "$queue"; then ok=true; else ok=false; fi + fi + [ "$ok" = true ] || failed=1 + results+="${results:+,}\"$id\":$ok" + done + if [ "$failed" = 0 ]; then status "done" "\"finished\":\"$(date -Is)\""; else status error "\"finished\":\"$(date -Is)\""; fi + usb_scan + FORCE=1 check + return $failed +} + +mkdir -p "$(dirname "$LOCK")" +exec 9>"$LOCK" +if [ "$mode" = --usb ]; then + case "$usbact" in + --usb-add) usb_add "$usbdev" || exit 1;; + --usb-remove) usb_remove "$usbdev";; + esac + flock -n 9 || { echo "== güncelleme sürüyor, sonunda taranacak / update running, rescans when done"; exit 0; } + usb_scan +elif [ "$mode" = --check ]; then + flock -n 9 || { echo "== başka bir güncelleme çalışıyor / another update is running"; exit 0; } + check +else + flock 9 + apply +fi diff --git a/scripts/download-content.sh b/scripts/download-content.sh index 0df02c9..935df27 100755 --- a/scripts/download-content.sh +++ b/scripts/download-content.sh @@ -12,6 +12,8 @@ # # Kullanım / usage: sudo ./scripts/download-content.sh [config-dosyası] # Ortam / environment: DEST=/library/zims/content RESERVE_GB=5 (boş bırakılacak pay / headroom to keep free) +# CHECK_ONLY=1 indirmez, yeni sürümleri "UPDATE " satırı olarak yazar +# downloads nothing, prints newer versions as "UPDATE " (auto-update.sh) set -uo pipefail [ "$EUID" -eq 0 ] || { echo "sudo ile çalıştır / run with sudo" >&2; exit 1; } @@ -56,6 +58,7 @@ while read -r folder prefix _; do # Yer kontrolü / free-space check: kalan boyut + pay / remaining bytes + headroom [ -z "$size" ] && size=$(curl -sIL -m 30 "$url" | grep -i '^content-length:' | tail -1 | tr -dc '0-9') + if [ -n "${CHECK_ONLY:-}" ]; then echo "UPDATE $folder $prefix $latest ${size:-?}"; continue; fi have=0; [ -f "$DEST/$latest.part" ] && have=$(stat -c %s "$DEST/$latest.part") avail=$(df -B1 --output=avail "$DEST" | tail -1 | tr -dc '0-9') if [ -n "$size" ] && [ "$avail" -lt $((size - have + RESERVE_GB * 1024 * 1024 * 1024)) ]; then @@ -91,6 +94,7 @@ while read -r folder prefix _; do [ "$old" != "$DEST/$latest" ] && [ -f "$old" ] && { echo "-- eski sürüm siliniyor / removing $old"; rm -f "$old"; } done done < "$CONF" +[ -n "${CHECK_ONLY:-}" ] && exit $failed echo ">> Kiwix kütüphanesi güncelleniyor / rebuilding Kiwix library" if command -v iiab-make-kiwix-lib >/dev/null; then diff --git a/scripts/download-map.sh b/scripts/download-map.sh new file mode 100755 index 0000000..95a6947 --- /dev/null +++ b/scripts/download-map.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# Çevrimdışı vektör harita: Protomaps planet build'inden bölge kesiti (PMTiles) + harita sayfası + yazı tipleri. +# Offline vector map: region extract (PMTiles) from the Protomaps planet build + map page + fonts. +# Her şey /library/www/html/harita/ altına yazılır; Internet-in-a-Box'ın nginx'i düz dosya olarak sunar +# (Range isteklerini kendisi karşılar, ek ayar yok): http://box.lan/harita/ +# Everything is written under /library/www/html/harita/; the IIAB nginx serves it as plain files +# (it handles Range requests itself, no extra config): http://box.lan/harita/ +# +# Neden / why: IIAB'ın harita indirmesi bazı ülkelerde (ör. Türkiye) engelli (README'deki contabostorage notu). +# Bu betik yalnızca build.protomaps.com, github.com ve cdn.jsdelivr.net'e gider. / This script only talks to +# build.protomaps.com, github.com and cdn.jsdelivr.net. +# +# Kullanım / usage: sudo ./scripts/download-map.sh [--update] [--force] +# (seçenek yok) harita yoksa indirir, varsa durumu bildirip çıkar / download if missing, otherwise report and exit +# --update daha yeni planet build'i varsa yeniden keser / re-extract when a newer build exists +# --force her durumda yeniden keser / always re-extract +# Ayarlar (ortam değişkeni / env): MAP_NAME (turkiye), MAP_BBOX (lonW,latS,lonE,latN), MAP_MAXZOOM (15), +# MAP_BUILD (YYYYMMDD; boş = en yeni / empty = latest), WEB (/library/www/html), RESERVE_GB (5) +# Örnek / example (Almanya / Germany): sudo MAP_NAME=almanya MAP_BBOX=5.8,47.2,15.1,55.1 ./scripts/download-map.sh +set -uo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WEB="${WEB:-/library/www/html}" +HARITA="$WEB/harita" +MAPS="$HARITA/data" +LIB="$HARITA/lib" +MAP_NAME="${MAP_NAME:-turkiye}" +MAP_BBOX="${MAP_BBOX:-25.5,35.7,45.0,42.2}" # Türkiye + kıyı/ada payı; ölçülen kesit 1,7 GB (2026-10-31 build, z15) +MAP_MAXZOOM="${MAP_MAXZOOM:-15}" +MAP_BUILD="${MAP_BUILD:-}" +RESERVE_GB="${RESERVE_GB:-5}" +PMTILES_VERSION=1.31.2 +# go-pmtiles yayını checksums dosyası vermiyor; sürüm sabitlenir, tarball sha256'ları burada tutulur (2026-11-01). +# The go-pmtiles release has no checksums file; version is pinned and tarball sha256 values are kept here. +PMTILES_SHA_x86_64=3ed7dbf4ec2e6dfe5e25b6f70d1ffc932729f93c86db353bf514dd71010a312f +PMTILES_SHA_arm64=f8bd47e7ea866863489cad588fbaf2f31f42e5821f7a03f009b3769f05801cb1 +BUILDS_URL=https://build-metadata.protomaps.dev/builds.json +BUILD_BASE=https://build.protomaps.com +ASSETS_URL=https://github.com/protomaps/basemaps-assets/archive/refs/heads/main.tar.gz +JSD=https://cdn.jsdelivr.net/npm +LOCK=/run/lock/harita-indir.lock + +UPDATE=0; FORCE=0 +for a in "$@"; do case "$a" in --update) UPDATE=1;; --force) FORCE=1;; *) echo "bilinmeyen seçenek / unknown option: $a" >&2; exit 2;; esac; done +[ "$EUID" -eq 0 ] || { echo "sudo ile çalıştır / run with sudo" >&2; exit 1; } +[ -d "$WEB" ] || { echo "$WEB yok. Önce Internet-in-a-Box'ı kur / install Internet-in-a-Box first" >&2; exit 1; } +for c in curl tar sha256sum flock; do command -v "$c" >/dev/null || { echo "$c gerekli / required" >&2; exit 1; }; done +exec 9>"$LOCK"; flock -n 9 || { echo "başka bir harita indirmesi çalışıyor / another map download is running ($LOCK)"; exit 0; } +mkdir -p "$MAPS" "$LIB" +failed=0 + +# --- sayfa + kütüphaneler / page + libraries ----------------------------------------------------------- +# Sürümler ve sha256'lar sabit; güncellerken ikisini birlikte değiştir. / Versions and hashes are pinned. +fetch_lib() { # dosya url sha256 + local f="$1" url="$2" sum="$3" + if [ -f "$LIB/$f" ] && echo "$sum $LIB/$f" | sha256sum -c --quiet 2>/dev/null; then return 0; fi + if curl -fsSL -m 300 -o "$LIB/$f.part" "$url" && echo "$sum $LIB/$f.part" | sha256sum -c --quiet; then + mv "$LIB/$f.part" "$LIB/$f" + else + echo "!! $f indirilemedi veya sha256 uyuşmuyor / download failed or sha256 mismatch" >&2 + rm -f "$LIB/$f.part"; failed=1 + fi +} +cp "$DIR/www/harita/index.html" "$HARITA/index.html" +cp "$DIR"/www/harita/lib/LICENSE-*.txt "$LIB/" +fetch_lib maplibre-gl.js "$JSD/maplibre-gl@5.24.0/dist/maplibre-gl.js" 45a9b07a9189ce56054c620a947ccf41e291e58c95e9b61533b740aaa65ee5cb +fetch_lib maplibre-gl.css "$JSD/maplibre-gl@5.24.0/dist/maplibre-gl.css" ab1e70d59ec40465bae7e7030da2f3ccf28133fd502e62bd598eefbadfd7a732 +fetch_lib pmtiles.js "$JSD/pmtiles@4.5.0/dist/pmtiles.js" caf981bc46f6327ee7e65d5dc964d89d38a69f60edca2bd4c5c890c21b554c6c +fetch_lib basemaps.js "$JSD/@protomaps/basemaps@5.7.2/dist/basemaps.js" 551864f01da1bfe3005e99987197f22f56d0c35f9a10fa1727a889e2434af16d +chmod -R a+rX "$HARITA" + +# --- pmtiles CLI (GitHub release, sürüm + sha256 sabit) ------------------------------------------------ +if ! command -v pmtiles >/dev/null; then + case "$(uname -m)" in + x86_64) arch=x86_64; sum=$PMTILES_SHA_x86_64;; + aarch64|arm64) arch=arm64; sum=$PMTILES_SHA_arm64;; + *) echo "!! desteklenmeyen mimari / unsupported arch: $(uname -m)" >&2; exit 1;; + esac + tgz="go-pmtiles_${PMTILES_VERSION}_Linux_${arch}.tar.gz" + tmp=$(mktemp -d) + echo "[$(date -Is)] pmtiles CLI indiriliyor / downloading: $tgz" + if ! curl -fsSL -m 300 -o "$tmp/$tgz" "https://github.com/protomaps/go-pmtiles/releases/download/v${PMTILES_VERSION}/$tgz"; then + echo "!! pmtiles indirilemedi / download failed" >&2; rm -rf "$tmp"; exit 1 + fi + if ! echo "$sum $tmp/$tgz" | sha256sum -c --quiet; then + echo "!! pmtiles sha256 uyuşmuyor, kurulmadı / checksum mismatch, not installed" >&2; rm -rf "$tmp"; exit 1 + fi + tar xzf "$tmp/$tgz" -C "$tmp" pmtiles && install -m 0755 "$tmp/pmtiles" /usr/local/bin/pmtiles + rm -rf "$tmp" +fi +echo "pmtiles: $(pmtiles version 2>&1 | head -1)" + +# --- en yeni planet build / latest planet build -------------------------------------------------------- +if [ -z "$MAP_BUILD" ]; then + MAP_BUILD=$(curl -fsSL -m 60 "$BUILDS_URL" | grep -oE '"key":"[0-9]{8}\.pmtiles"' | grep -oE '[0-9]{8}' | sort | tail -1) + [ -n "$MAP_BUILD" ] || { echo "!! build listesi alınamadı / cannot read $BUILDS_URL" >&2; exit 1; } +fi +OUT="$MAPS/$MAP_NAME.pmtiles"; META="$MAPS/map.json" +have=""; [ -f "$META" ] && have=$(grep -oE '"build": *"[0-9]{8}"' "$META" | grep -oE '[0-9]{8}') + +extract=1 +if [ -f "$OUT" ] && [ "$FORCE" = 0 ]; then + if [ "$UPDATE" = 0 ]; then + echo "== $OUT var (yapı/build ${have:-?}, en yeni/latest $MAP_BUILD); yenilemek için / to refresh: --update"; extract=0 + elif [ -n "$have" ] && [ "$have" -ge "$MAP_BUILD" ]; then + echo "== $OUT zaten en yeni yapı / already latest build ($have)"; extract=0 + fi +fi + +if [ "$extract" = 1 ]; then + avail=$(df -B1 --output=avail "$MAPS" | tail -1 | tr -dc '0-9') + if [ "$avail" -lt $(( (RESERVE_GB + 3) * 1024*1024*1024 )) ]; then + echo "!! $MAPS için yer yok / not enough space (boş/free $((avail/1024/1024)) MB; kesit/extract ~2 GB + $RESERVE_GB GB pay/headroom)" >&2; exit 1 + fi + src="$BUILD_BASE/$MAP_BUILD.pmtiles" + echo "[$(date -Is)] >> $MAP_NAME kesiliyor / extracting: build $MAP_BUILD, bbox $MAP_BBOX, maxzoom $MAP_MAXZOOM (Türkiye ~1,7 GB, 2-10 dk)" + rm -f "$OUT.part" + # İlerleme çubuğu \r ile aynı satırı yeniler; log'a yüzlerce KB çöp yazmasın diye süzülür. + # The progress bar rewrites one line with \r; filter it so logs do not fill with noise. + pmtiles extract "$src" "$OUT.part" --bbox="$MAP_BBOX" --maxzoom="$MAP_MAXZOOM" --download-threads=4 < /dev/null 2>&1 \ + | tr '\r' '\n' | grep -vE '^ *fetching chunks|^[[:space:]]*$' | sed 's/^/ /' + rc=${PIPESTATUS[0]} + if [ "$rc" = 0 ] && pmtiles verify "$OUT.part" >/dev/null 2>&1; then + mv "$OUT.part" "$OUT"; chmod 0644 "$OUT" + bytes=$(stat -c %s "$OUT") + printf '{"name":"%s","build":"%s","bbox":[%s],"maxzoom":%s,"bytes":%s,"updated":"%s","source":"%s"}\n' \ + "$MAP_NAME" "$MAP_BUILD" "$MAP_BBOX" "$MAP_MAXZOOM" "$bytes" "$(date -Is)" "$src" > "$META" + chmod 0644 "$META" + echo "[$(date -Is)] OK $OUT ($((bytes/1024/1024)) MB)" + pmtiles show "$OUT" 2>/dev/null | grep -E 'tile entries|zoom|bounds' | sed 's/^/ /' + else + echo "!! kesit başarısız veya doğrulanamadı, .part silindi / extract failed or did not verify" >&2 + rm -f "$OUT.part"; failed=1 + fi +fi + +# --- yazı tipleri + sprite'lar (basemaps-assets, ~6 MB) / fonts + sprites ------------------------------ +if [ ! -f "$MAPS/assets/sprites/v4/light.json" ] || [ ! -f "$MAPS/assets/fonts/Noto Sans Regular/0-255.pbf" ]; then + tmp=$(mktemp -d) + echo "[$(date -Is)] >> yazı tipi/sprite paketi indiriliyor / downloading fonts and sprites" + if curl -fsSL -m 300 -o "$tmp/assets.tgz" "$ASSETS_URL" \ + && mkdir -p "$MAPS/assets" \ + && tar xzf "$tmp/assets.tgz" -C "$MAPS/assets" --strip-components=1 --wildcards \ + 'basemaps-assets-main/fonts/Noto Sans Regular/*' 'basemaps-assets-main/fonts/Noto Sans Medium/*' \ + 'basemaps-assets-main/fonts/Noto Sans Italic/*' 'basemaps-assets-main/fonts/OFL.txt' \ + 'basemaps-assets-main/sprites/v4/*' 'basemaps-assets-main/README.md'; then + chmod -R a+rX "$MAPS/assets" + echo " OK fonts: $(find "$MAPS/assets/fonts" -name '*.pbf' | wc -l) pbf, sprites: $(ls "$MAPS/assets/sprites/v4" | wc -l) dosya/files" + else + echo "!! paket indirilemedi; etiketler ve simgeler görünmez (tekrar çalıştır) / assets failed, labels and icons will be missing" >&2 + failed=1 + fi + rm -rf "$tmp" +fi + +chmod -R a+rX "$HARITA" +echo "[$(date -Is)] bitti / done (failed=$failed)"; df -h "$MAPS" | tail -1 +[ -f "$OUT" ] && echo "Harita / map: http://box.lan/harita/" +exit $failed diff --git a/scripts/install-auto-update.sh b/scripts/install-auto-update.sh new file mode 100755 index 0000000..bdfb4a2 --- /dev/null +++ b/scripts/install-auto-update.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Otomatik güncellemeyi kurar: internet gelince yeni sürüm kontrolü, ana sayfada bildirim ve +# "Güncelle" butonu. Hiçbir şey onaysız indirilmez. Tekrar çalıştırmak güvenlidir. +# Installs automatic updates: a check for newer versions when internet appears, a homepage notice +# and an update button. Nothing is downloaded without approval. Safe to re-run. +# +# Güncelleme butonu iiab-admin şifresini ister / the update button asks for the iiab-admin password. +# +# Kullanım / usage: sudo ./scripts/install-auto-update.sh +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +SRC="$REPO/update" +APP=/opt/has-update +NGINX_CONF=/etc/nginx/conf.d/has-update-nginx.conf +UNITS="has-update-check.service has-update-check.timer has-update-apply.service has-update-apply.path has-update-api.service has-update-admin.service has-update-admin.path has-usb@.service" + +die(){ echo "HATA / ERROR: $*" >&2; exit 1; } +[ "$EUID" -eq 0 ] || die "sudo ile çalıştır / run with sudo: sudo ./scripts/install-auto-update.sh" +[ -d /etc/nginx/conf.d ] || die "nginx bulunamadı. Önce / install first: sudo ./install.sh" + +install -d -m 755 "$APP" +install -m 644 "$SRC/update-api.py" "$APP/" +install -m 755 "$SRC/admin-hash.sh" "$APP/" +# Servisler betikleri bu repodan çalıştırır, config/content.conf değişiklikleri de böylece geçerli olur. +# The services run the scripts from this repo, so edits to config/content.conf apply too. +for u in $UNITS; do + sed "s#@REPO@#$REPO#g" "$SRC/$u" > "/etc/systemd/system/$u" + chmod 644 "/etc/systemd/system/$u" +done + +if [ -d /etc/NetworkManager/dispatcher.d ]; then + install -m 755 "$SRC/90-has-update" /etc/NetworkManager/dispatcher.d/90-has-update +else + echo "Uyarı / warning: NetworkManager yok, kontrol yalnızca günde bir yapılır / no NetworkManager, checks run daily only" +fi + +install -m 644 "$SRC/99-has-usb.rules" /etc/udev/rules.d/99-has-usb.rules +udevadm control --reload +ln -sfn /run/has-usb/usb.json /library/www/html/home/usb.json + +systemctl daemon-reload +systemctl enable --now has-update-api.service has-update-apply.path has-update-admin.path has-update-check.timer >/dev/null +systemctl restart has-update-api.service + +install -m 644 "$SRC/update-nginx.conf" "$NGINX_CONF" +if nginx -t 2>/dev/null; then + systemctl reload nginx +else + rm -f "$NGINX_CONF" + nginx -t || true + die "nginx ayarı hatalı, geri alındı / nginx config failed and was removed" +fi + +"$REPO/scripts/install-homepage.sh" +systemctl start --no-block has-update-check.service + +cat < "$D/admin.tmp" +chown --reference="$D" "$D/admin.tmp" +chmod 600 "$D/admin.tmp" +mv "$D/admin.tmp" "$D/admin" diff --git a/update/has-update-admin.path b/update/has-update-admin.path new file mode 100644 index 0000000..c5497f2 --- /dev/null +++ b/update/has-update-admin.path @@ -0,0 +1,9 @@ +[Unit] +Description=HAS_PrepperDisk: iiab-admin şifresi değişince güncelleme butonuna bildir / pass a changed iiab-admin password to the update button + +[Path] +PathChanged=/etc/shadow +Unit=has-update-admin.service + +[Install] +WantedBy=paths.target diff --git a/update/has-update-admin.service b/update/has-update-admin.service new file mode 100644 index 0000000..06b566c --- /dev/null +++ b/update/has-update-admin.service @@ -0,0 +1,6 @@ +[Unit] +Description=HAS_PrepperDisk: iiab-admin şifre özetini yenile / refresh the iiab-admin password hash + +[Service] +Type=oneshot +ExecStart=/opt/has-update/admin-hash.sh diff --git a/update/has-update-api.service b/update/has-update-api.service new file mode 100644 index 0000000..13ba63d --- /dev/null +++ b/update/has-update-api.service @@ -0,0 +1,28 @@ +[Unit] +Description=HAS_PrepperDisk: ana sayfadaki güncelleme butonu / homepage update button +After=network.target local-fs.target +RequiresMountsFor=/library + +[Service] +Type=simple +DynamicUser=yes +RuntimeDirectory=has-update +ExecStartPre=+/opt/has-update/admin-hash.sh +Environment=PYTHONUNBUFFERED=1 +ExecStart=/usr/bin/python3 /opt/has-update/update-api.py +Restart=always +RestartSec=5 + +# Sıkılaştırma / hardening +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +LockPersonality=yes + +[Install] +WantedBy=multi-user.target diff --git a/update/has-update-apply.path b/update/has-update-apply.path new file mode 100644 index 0000000..2dfafde --- /dev/null +++ b/update/has-update-apply.path @@ -0,0 +1,9 @@ +[Unit] +Description=HAS_PrepperDisk: ana sayfadan gelen güncelleme isteğini bekle / wait for update requests from the homepage + +[Path] +PathExists=/run/has-update/request +Unit=has-update-apply.service + +[Install] +WantedBy=paths.target diff --git a/update/has-update-apply.service b/update/has-update-apply.service new file mode 100644 index 0000000..639e4c5 --- /dev/null +++ b/update/has-update-apply.service @@ -0,0 +1,10 @@ +[Unit] +Description=HAS_PrepperDisk: ana sayfada onaylanan güncellemeleri indir / download updates approved on the homepage +RequiresMountsFor=/library + +[Service] +Type=oneshot +ExecStart=@REPO@/scripts/auto-update.sh --apply +# İndirme sürerken kütüphane akıcı kalsın / keep the library responsive while downloading +Nice=10 +IOSchedulingClass=idle diff --git a/update/has-update-check.service b/update/has-update-check.service new file mode 100644 index 0000000..0b0129d --- /dev/null +++ b/update/has-update-check.service @@ -0,0 +1,8 @@ +[Unit] +Description=HAS_PrepperDisk: yeni içerik sürümü kontrolü / check for newer content versions +After=network-online.target local-fs.target +RequiresMountsFor=/library + +[Service] +Type=oneshot +ExecStart=@REPO@/scripts/auto-update.sh --check diff --git a/update/has-update-check.timer b/update/has-update-check.timer new file mode 100644 index 0000000..8d26cfe --- /dev/null +++ b/update/has-update-check.timer @@ -0,0 +1,14 @@ +# Asıl tetikleyici NetworkManager'dır (internet gelince). Bu zamanlayıcı sürekli bağlı +# cihazlar içindir: bağlantı hiç kopmazsa yeni bir "internet geldi" olayı da olmaz. +# NetworkManager is the main trigger (when internet appears). This timer is for always-online +# boxes: if the connection never drops there is no new "internet appeared" event. +[Unit] +Description=HAS_PrepperDisk: günlük güncelleme kontrolü / daily update check + +[Timer] +OnBootSec=15min +OnUnitActiveSec=1d +RandomizedDelaySec=1h + +[Install] +WantedBy=timers.target diff --git a/update/has-usb@.service b/update/has-usb@.service new file mode 100644 index 0000000..2fcc292 --- /dev/null +++ b/update/has-usb@.service @@ -0,0 +1,10 @@ +[Unit] +Description=HAS_PrepperDisk: USB diskindeki içerikleri tara / scan USB drive %I for content +BindsTo=dev-%i.device +After=dev-%i.device local-fs.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=@REPO@/scripts/auto-update.sh --usb-add %I +ExecStop=@REPO@/scripts/auto-update.sh --usb-remove %I diff --git a/update/update-api.py b/update/update-api.py new file mode 100644 index 0000000..33568d4 --- /dev/null +++ b/update/update-api.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""HAS_PrepperDisk güncelleme butonu: POST http://box.lan/guncelleme/ {"ids": [...]} + +Ana sayfadaki "Güncelle" butonu buraya istek atar; iiab-admin şifresi gerekir. Servis yetkisiz bir kullanıcıyla çalışır ve +hiçbir şey indirmez: seçimi updates.json ve usb.json'a göre denetleyip istek dosyasına yazar, indirmeyi +has-update-apply.path (root) başlatır. + +The homepage "Güncelle" button posts here; the iiab-admin password is required. The service runs unprivileged and downloads nothing: +it checks the selection against updates.json and usb.json and writes the request file; has-update-apply.path +(root) starts the download. +""" +import ctypes +import ctypes.util +import hmac +import json +import os +import threading +import time +from collections import defaultdict +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +WEB = os.environ.get("WEB", "/library/www/html") +UPDATES = os.path.join(WEB, "home", "updates.json") +USB = os.path.join(WEB, "home", "usb.json") +STATUS = os.path.join(WEB, "home", "update-status.json") +REQUEST = os.environ.get("REQUEST", "/run/has-update/request") +PORT = int(os.environ.get("PORT", "8097")) +# iiab-admin şifresinin /etc/shadow'daki özeti; admin-hash.sh yazar. / iiab-admin's hash from /etc/shadow, written by admin-hash.sh. +ADMIN = os.path.join(os.path.dirname(REQUEST), "admin") +# 10 dakikada cihaz başına 5, IP değiştirmeye karşı tüm cihazlar için toplam 20 yanlış şifre. +# Per 10 minutes: 5 wrong passwords per device, 20 across all devices so changing IP does not help. +RATE_N, RATE_ALL, RATE_WINDOW = 5, 20, 600 + +lock = threading.Lock() +fails = defaultdict(list) + +# Python 3.13'te crypt modülü yok; yescrypt özetini sistemin libcrypt'i doğrular. +# Python 3.13 has no crypt module; the system libcrypt checks the yescrypt hash. +libcrypt = ctypes.CDLL(ctypes.util.find_library("crypt")) +libcrypt.crypt.restype = ctypes.c_char_p +libcrypt.crypt.argtypes = (ctypes.c_char_p, ctypes.c_char_p) + + +def read_json(path): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError): + return {} + + +def password_ok(pw): + try: + with open(ADMIN, encoding="utf-8") as f: + h = f.read().strip() + except OSError: + return False + # Şifresi kilitli ya da boş hesap ("!", "*", "") hiçbir şifreyle açılmaz / locked or empty accounts never match + if not h.startswith("$"): + return False + with lock: + r = libcrypt.crypt(pw.encode(), h.encode()) + return bool(r) and hmac.compare_digest(r.decode(), h) + + +def running(): + # auto-update.sh indirirken durumu 3 sn'de bir yazar; 2 dk sessizse yarıda kalmıştır. + # auto-update.sh rewrites the status every 3 s while downloading; 2 min of silence means it died. + if os.path.exists(REQUEST): + return True + try: + fresh = time.time() - os.path.getmtime(STATUS) < 120 + except OSError: + return False + return fresh and read_json(STATUS).get("state") == "running" + + +class Handler(BaseHTTPRequestHandler): + def _send(self, status, payload): + body = json.dumps(payload, ensure_ascii=False).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_POST(self): + # Yalnızca JSON: başka bir sitenin bu isteği kullanıcının tarayıcısından atması CORS ön kontrolüne takılır. + # JSON only: another site cannot send this from a visitor's browser without a CORS preflight we never answer. + if self.headers.get("Content-Type", "").split(";")[0].strip() != "application/json": + return self._send(415, {"error": "application/json gerekli / required"}) + try: + length = min(int(self.headers.get("Content-Length", 0)), 4096) + data = json.loads(self.rfile.read(length)) + ids, pw = data["ids"], data.get("sifre") + except (ValueError, KeyError, TypeError): + return self._send(400, {"error": "Geçersiz istek / bad request"}) + ip, now = self.headers.get("X-Real-IP") or self.client_address[0], time.time() + with lock: + for k in (ip, "*"): + fails[k] = [t for t in fails[k] if now - t < RATE_WINDOW] + if len(fails[ip]) >= RATE_N or len(fails["*"]) >= RATE_ALL: + return self._send(429, {"error": "Çok fazla yanlış şifre, biraz bekleyin / too many wrong passwords, wait a bit"}) + if not isinstance(pw, str) or not password_ok(pw): + with lock: + fails[ip].append(now) + fails["*"].append(now) + return self._send(403, {"error": "Şifre yanlış / wrong password"}) + if running(): + return self._send(409, {"error": "Güncelleme zaten sürüyor / an update is already running"}) + items = read_json(UPDATES).get("items", []) + read_json(USB).get("items", []) + allowed = {i.get("id") for i in items if i.get("fits")} + if not isinstance(ids, list) or not ids or not all(isinstance(i, str) and i in allowed for i in ids): + return self._send(400, {"error": "Seçim geçersiz ya da diskte yer yok / invalid selection or no space"}) + tmp = REQUEST + ".tmp" + with open(tmp, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(dict.fromkeys(ids)) + "\n") + os.replace(tmp, REQUEST) + self._send(202, {"ok": True}) + + def log_message(self, fmt, *args): + print("%s - %s" % (self.headers.get("X-Real-IP") or self.address_string(), fmt % args), flush=True) + + +if __name__ == "__main__": + ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever() diff --git a/update/update-nginx.conf b/update/update-nginx.conf new file mode 100644 index 0000000..289b102 --- /dev/null +++ b/update/update-nginx.conf @@ -0,0 +1,9 @@ +# HAS_PrepperDisk güncelleme butonu: POST http://box.lan/guncelleme/ -> 127.0.0.1:8097 +# Internet-in-a-Box bu klasördeki dosyaları server bloğunun içine ekler. +# Internet-in-a-Box includes files from /etc/nginx/conf.d inside its server block. +location ^~ /guncelleme/ { + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 4k; + proxy_read_timeout 10s; + proxy_pass http://127.0.0.1:8097/; +} diff --git a/www/harita/index.html b/www/harita/index.html new file mode 100644 index 0000000..d9c1f68 --- /dev/null +++ b/www/harita/index.html @@ -0,0 +1,136 @@ + + + + + +Harita :: HAS_PrepperDisk + + + + +
+ « Ana sayfa + Çevrimdışı Harita + + +
+ +
+ + +
+
+
+
+ + + + + + + diff --git a/www/harita/lib/LICENSE-basemaps.txt b/www/harita/lib/LICENSE-basemaps.txt new file mode 100644 index 0000000..8441b08 --- /dev/null +++ b/www/harita/lib/LICENSE-basemaps.txt @@ -0,0 +1,54 @@ +# Code Licenses and Attribution + +# The BSD 3-Clause License (BSD-3-Clause) + +Copyright 2019-2023 Protomaps LLC, Kelso Cartography + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Visual Design + +The visual design of the Protomaps basemap styles was created by Geraldine Sarmiento for Protomaps LLC and released under a Creative Commons 0 (CC0) license. + +https://creativecommons.org/publicdomain/zero/1.0/ + +Certain map styles may also include derivatives of icons designed for Mapzen and released under an MIT license. + +https://github.com/tangrams/icons/blob/master/LICENSE.md + +# Tile Schema + +The organization of layers and tags is adapted from the Tilezen project. + +https://github.com/tilezen/vector-datasource +https://github.com/tilezen/vector-datasource/blob/master/docs/LICENSE-SCHEMA.md + +# The MIT License (MIT) + +Copyright (c) 2015-2018 Mapzen +Copyright (c) 2019 Linux Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/www/harita/lib/LICENSE-maplibre.txt b/www/harita/lib/LICENSE-maplibre.txt new file mode 100644 index 0000000..1e8acbb --- /dev/null +++ b/www/harita/lib/LICENSE-maplibre.txt @@ -0,0 +1,116 @@ +Copyright (c) 2023, MapLibre contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of MapLibre GL JS nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------- + +Contains code from mapbox-gl-js v1.13 and earlier + +Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license + +Copyright (c) 2020, Mapbox +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Mapbox GL JS nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------- + +Contains code from glfx.js + +Copyright (C) 2011 by Evan Wallace + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- + +Contains a portion of d3-color https://github.com/d3/d3-color + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/www/harita/lib/LICENSE-pmtiles.txt b/www/harita/lib/LICENSE-pmtiles.txt new file mode 100644 index 0000000..ec3d69a --- /dev/null +++ b/www/harita/lib/LICENSE-pmtiles.txt @@ -0,0 +1,19 @@ +The below license (BSD-3) applies to the reference implementations in this repository. + +The PMTiles specification itself is public domain, or CC0 where applicable. + +Sample tilesets available in this repository are subject to their own license terms. + +--- + +Copyright 2021 Protomaps LLC + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/www/home/index.html b/www/home/index.html index da3e44c..b7f0a20 100644 --- a/www/home/index.html +++ b/www/home/index.html @@ -56,6 +56,7 @@ .search td{background:#EFEFEF} .search input[type=text],.search select{font-family:Verdana,sans-serif;font-size:11px;border:1px solid #7F9DB9;padding:2px;background:#fff} .search input[type=text]{width:220px;max-width:100%} + #upd-pw{font-family:Verdana,sans-serif;font-size:11px;border:1px solid #7F9DB9;padding:2px;width:130px} .btn{font-family:Verdana,sans-serif;font-size:11px;font-weight:bold;background:#FAFAFA;border:1px solid #000;padding:1px 6px;cursor:pointer} .btn:hover{background:#FFF3D6} @@ -63,6 +64,13 @@ .legend span{display:flex;align-items:center;gap:6px} .legend .fold{margin-top:0;transform:scale(.8)} + /* güncellemeler */ + .warn{color:#B00000;font-weight:bold} + .bar{height:8px;background:#fff;border:1px solid #7F9DB9;margin-top:4px;max-width:400px} + .bar i{display:block;height:100%;background:linear-gradient(#FFA34F,#DD6900)} + /* 16 px altı yazıda iPhone kutuya dokununca sayfayı yakınlaştırır / iPhone zooms into inputs under 16 px */ + @media (max-width:640px){ #upd-pw{font-size:16px;padding:6px;width:100%;box-sizing:border-box;margin-bottom:6px} } + #disk td{color:#B00000;font-weight:bold;font-size:12px} .copy{text-align:center;font-size:10px;color:#444;margin:12px 0 4px;line-height:1.6} @@ -113,6 +121,8 @@
+
+
@@ -162,6 +172,7 @@ ]}, { title:"Haritalar ve Navigasyon", items:[ { name:"Dünya Haritası", desc:"Avrupa ve Orta Doğu sokak seviyesinde, dünyanın geri kalanı şehir seviyesinde.", lang:"—", size:"~40 GB", url:"/maps" }, + { name:"Çevrimdışı Harita (Protomaps)", desc:"Türkiye vektör haritası, 15. yakınlaştırma düzeyine kadar, Türkçe etiketli. Yüklü değilse sayfa kurulum komutunu gösterir.", lang:"TR", size:"~1,7 GB", url:"/harita/" }, ]}, { title:"Hayatta Kalma ve Hazırlık", items:[ { name:"Afet Sonrası Kitaplığı", desc:"Barınak, enerji, tarım ve yeniden yapılanma üzerine kitaplar.", lang:"EN", size:"~615 MB", zim:"zimgit-post-disaster_en", search:true }, @@ -292,7 +303,124 @@ box.hidden = false; }).catch(() => {}); +/* Güncellemeler. scripts/auto-update.sh --check yeni sürümleri updates.json'a yazar; butona basınca + seçilenler /guncelleme/ adresine gider, indirme update-status.json'dan izlenir. */ +const getJSON = u => fetch(u, {cache:"no-store"}).then(r => r.ok ? r.json() : null).catch(() => null); +const fmtSize = b => b == null ? "?" : b >= 1e9 ? (b / 1e9).toFixed(1).replace(".", ",") + " GB" : Math.max(1, Math.round(b / 1e6)) + " MB"; +const fmtDate = s => new Date(s).toLocaleString("tr-TR", {dateStyle:"short", timeStyle:"short"}); +function fmtVer(s){ + let m = /_(\d{4})-(\d{2})[a-z]?\.zim$/.exec(s || ""); + if (m) return `${m[2]}/${m[1]}`; + m = /^(\d{4})(\d{2})(\d{2})$/.exec(s || ""); + return m ? `${m[3]}.${m[2]}.${m[1]}` : "yüklü değil"; +} +function itemName(id, items){ + const i = (items || []).find(x => x.id === id) || {}; + if (i.kind === "map") return `Çevrimdışı Harita (${i.name})`; + const usb = id.startsWith("usb."), key = usb ? id.slice(4) : id; + const c = CATS.flatMap(c => c.items).find(x => x.zim && key.startsWith(x.zim)); + return (c ? c.name : key) + (usb ? " (USB'den)" : ""); +} + +async function loadUpdates(){ + const [u, usb, s] = await Promise.all([getJSON("/home/updates.json"), getJSON("/home/usb.json"), getJSON("/home/update-status.json")]); + const box = document.getElementById("upd"), items = [...((usb && usb.items) || []), ...((u && u.items) || [])]; + const disk = usb || u; + let html = ""; + if (s && s.state === "running" && Date.now() - new Date(s.updated) < 120000) { + const c = s.current || {}, pct = c.size ? Math.min(100, Math.floor(c.done * 100 / c.size)) : null; + html = ``; + box.innerHTML = html; box.hidden = false; + setTimeout(loadUpdates, 3000); + return; + } + if (s && s.finished && Date.now() - new Date(s.finished) < 864e5) { + const r = Object.entries(s.results || {}), bad = r.filter(([, ok]) => !ok).map(([id]) => itemName(id, items)); + html += ``; + } + if (items.length) { + /* Kutuda hiç olmayan içerikler (yeni kurulumda hepsi) listeyi doldurmasın: kapalı durur, seçili gelmez. + Content the box never had (all of it on a fresh install) stays folded and unselected. */ + const eksik = i => i.kind === "zim" && !i.current && !i.id.startsWith("usb."); + const yeni = items.filter(i => !eksik(i)), yok = items.filter(eksik); + html = `` + html; + // Varsayılan seçim toplamda sığanlarla sınırlı / pre-select only what fits together + let room = disk.free - (disk.reserve || 0); + if (yok.length) yeni.push({toggle: true}); + for (const i of [...yeni, ...yok]) { + if (i.toggle) { + const boyut = fmtSize(yok.reduce((t, x) => t + (x.size || 0), 0)); + html += ``; + continue; + } + const pick = !eksik(i) && i.fits && (i.size || 0) <= room; + if (pick) room -= i.size || 0; + html += ` + + + + `; + } + box.dataset.room = disk.free - (disk.reserve || 0); + html += ``; + } else if (html) { + html = `` + html; + } + box.innerHTML = html; box.hidden = !html; + updTotal(); +} + +function updTotal(){ + const el = document.getElementById("upd-total"); + if (!el) return; + const sel = [...document.querySelectorAll("#upd input:checked")]; + const total = sel.reduce((t, c) => t + +c.dataset.size, 0), over = total > +document.getElementById("upd").dataset.room; + el.textContent = sel.length ? `Toplam ${fmtSize(total)}` + (over ? " · Seçilenlerin hepsi diske sığmıyor, birkaçını kaldırın." : "") : ""; + el.className = over ? "warn" : ""; + document.getElementById("upd-go").disabled = !sel.length || over; +} + +async function startUpdate(){ + const sel = [...document.querySelectorAll("#upd input:checked")]; + const pw = document.getElementById("upd-pw").value; + if (!sel.length) return; + if (!pw) { alert("Güncellemek için iiab-admin şifresini girin (yönetim paneli şifresi)."); return; } + const total = sel.reduce((t, c) => t + +c.dataset.size, 0); + if (!confirm(`${sel.length} içerik, toplam ${fmtSize(total)} indirilecek. Başlatılsın mı?`)) return; + document.getElementById("upd-go").disabled = true; + try { + const r = await fetch("/guncelleme/", {method:"POST", headers:{"Content-Type":"application/json"}, + body: JSON.stringify({ids: sel.map(c => c.value), sifre: pw})}); + if (!r.ok) alert((await r.json().catch(() => ({}))).error || "Güncelleme başlatılamadı."); + } catch(e) { alert("Güncelleme servisine ulaşılamadı."); } + setTimeout(loadUpdates, 2000); +} + +document.getElementById("upd").addEventListener("change", updTotal); +document.getElementById("upd").addEventListener("click", e => { + if (e.target.id === "upd-go") startUpdate(); + if (e.target.id === "upd-more") { + e.preventDefault(); + const rows = document.querySelectorAll("#upd tr.eksik"), open = rows.length && rows[0].hidden; + rows.forEach(r => { r.hidden = !open; }); + e.target.textContent = e.target.textContent.replace(open ? "göster" : "gizle", open ? "gizle" : "göster"); + } +}); + loadBooks().then(render); +loadUpdates();
Güncelleme sürüyor
+ ${esc(itemName(c.id || "", items))} +
${esc(fmtSize(c.done))}${pct === null ? "" : ` / ${esc(fmtSize(c.size))} (%${pct})`} + ${s.queue && s.queue.length ? ` · sırada ${s.queue.length} içerik daha` : ""}
+ ${pct === null ? "" : `
`} +
Bu sırada kütüphaneyi kullanmaya devam edebilirsiniz.
Son güncelleme (${esc(fmtDate(s.finished))}): + ${r.length - bad.length} içerik güncellendi${bad.length ? `, ${bad.length} tanesi başarısız: ${esc(bad.join(", "))}` : "."}
${yeni.length ? "Yeni sürüm var" : "Kutuda olmayan içerikler"}SürümBoyut
Kutuda olmayan ${yok.length} içerik (toplam ${esc(boyut)}): göster
${esc(itemName(i.id, items))} + ${i.fits ? "" : `
Diskte yer yok.${i.current ? " Eski sürüm kullanılmaya devam ediyor." : ""}
`}
${esc(fmtVer(i.current))} → ${esc(fmtVer(i.latest))}${esc(fmtSize(i.size))}
+ ${u && u.metered && u.items.length ? `
Pi şu anda kotalı bir bağlantıda (ör. telefondan internet paylaşımı). İndirme mobil veri harcar.
` : ""} + ${usb && usb.items.length ? `
USB'den gelen içerikleri yalnızca güvendiğiniz bir kaynaktan geldiyse seçin. Dosyanın bozuk olmadığı doğrulanır, ama kimin hazırladığı doğrulanamaz.
` : ""} + + + ${u ? ` · Son kontrol: ${esc(fmtDate(u.checked))}` : ""}
Güncellemeler