|
| 1 | +/** |
| 2 | + * The landing site, plus a counted redirect for every download link. |
| 3 | + * |
| 4 | + * Downloads are served by GitHub, not by us — the assets are large, GitHub is |
| 5 | + * more reliable than anything here, and its own per-asset counter is worth |
| 6 | + * keeping. But a link straight to `github.com` leaves no trace on this side, so |
| 7 | + * the site cannot tell whether a visit turned into a download. `/dl/<platform>` |
| 8 | + * is a doorway: it records that a click happened and then hands the visitor |
| 9 | + * straight to the same GitHub URL they would have got anyway. |
| 10 | + * |
| 11 | + * What is written down is a running count per day, platform and country. There |
| 12 | + * is no row per person: no address, no user agent, no identifier, nothing that |
| 13 | + * describes who downloaded, only how many did. |
| 14 | + */ |
| 15 | + |
| 16 | +const OWNER_REPO = "adarshaacharya/sajilo"; |
| 17 | +const RELEASES_LATEST = `https://github.com/${OWNER_REPO}/releases/latest`; |
| 18 | + |
| 19 | +/** The asset each `/dl/<platform>` link stands for. */ |
| 20 | +const ASSETS = { |
| 21 | + "macos-arm64": "Sajilo-macos-arm64.dmg", |
| 22 | + "macos-x64": "Sajilo-macos-x64.dmg", |
| 23 | + windows: "Sajilo-windows-x64.exe", |
| 24 | + "linux-deb": "Sajilo-linux-amd64.deb", |
| 25 | + "linux-appimage": "Sajilo-linux-x86_64.AppImage", |
| 26 | +}; |
| 27 | + |
| 28 | +function assetUrl(name) { |
| 29 | + return `https://github.com/${OWNER_REPO}/releases/latest/download/${name}`; |
| 30 | +} |
| 31 | + |
| 32 | +export default { |
| 33 | + async fetch(request, env, ctx) { |
| 34 | + const url = new URL(request.url); |
| 35 | + const platform = url.pathname.startsWith("/dl/") |
| 36 | + ? url.pathname.slice("/dl/".length) |
| 37 | + : null; |
| 38 | + |
| 39 | + if (platform === null) { |
| 40 | + return env.ASSETS.fetch(request); |
| 41 | + } |
| 42 | + |
| 43 | + const asset = ASSETS[platform]; |
| 44 | + // An unknown platform still gets somewhere useful. A 404 here would mean a |
| 45 | + // stale link on a shared post silently costing a download. |
| 46 | + const target = asset ? assetUrl(asset) : RELEASES_LATEST; |
| 47 | + |
| 48 | + if (asset) { |
| 49 | + // `waitUntil` keeps the redirect immediate, and keeps a database that is |
| 50 | + // down or over quota from ever standing between a visitor and the file. |
| 51 | + ctx.waitUntil(record(env, platform, request)); |
| 52 | + } |
| 53 | + |
| 54 | + return new Response(null, { |
| 55 | + status: 302, |
| 56 | + headers: { |
| 57 | + Location: target, |
| 58 | + // Each click is a click. A cached redirect would be counted once and |
| 59 | + // then never again for that visitor. |
| 60 | + "Cache-Control": "no-store", |
| 61 | + }, |
| 62 | + }); |
| 63 | + }, |
| 64 | +}; |
| 65 | + |
| 66 | +async function record(env, platform, request) { |
| 67 | + if (!env.DB) return; |
| 68 | + |
| 69 | + const day = new Date().toISOString().slice(0, 10); |
| 70 | + const country = request.cf?.country ?? "XX"; |
| 71 | + const referrer = referrerHost(request.headers.get("Referer")); |
| 72 | + |
| 73 | + try { |
| 74 | + await env.DB.prepare( |
| 75 | + `INSERT INTO download_clicks (day, platform, country, referrer, clicks) |
| 76 | + VALUES (?1, ?2, ?3, ?4, 1) |
| 77 | + ON CONFLICT (day, platform, country, referrer) |
| 78 | + DO UPDATE SET clicks = clicks + 1`, |
| 79 | + ) |
| 80 | + .bind(day, platform, country, referrer) |
| 81 | + .run(); |
| 82 | + } catch { |
| 83 | + // A missed tally is not worth a failed download. |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * Which site sent them, and nothing more. |
| 89 | + * |
| 90 | + * A full referring URL can carry the search terms someone typed or the path of |
| 91 | + * a private page, neither of which this is entitled to keep. The hostname |
| 92 | + * answers the only question being asked — did that Reddit post work — so it is |
| 93 | + * all that is kept. Our own pages read as "direct": a visit that began on the |
| 94 | + * site is not a referral to it. |
| 95 | + */ |
| 96 | +function referrerHost(header) { |
| 97 | + if (!header) return "direct"; |
| 98 | + try { |
| 99 | + const host = new URL(header).hostname.replace(/^www\./, ""); |
| 100 | + return host === "sajilo.fyi" ? "direct" : host; |
| 101 | + } catch { |
| 102 | + return "direct"; |
| 103 | + } |
| 104 | +} |
0 commit comments