diff --git a/AGENTS.md b/AGENTS.md index aab19b7a6..6809fd6c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ A coding agent (Claude Code, Cursor, Aider, Codex, ...) is reading this. Read on ## What it is -23 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. +24 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. Two repos: - **code** (this one) — Flask apps, control plane, scripts. @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn//`. The path predates the ren # fresh clone ./scripts/fetch_assets.sh # pulls assets from HF ./scripts/build.sh # docker build -t webharbor:dev . -docker run -d -p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev +docker run -d -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev ``` Or use the published image directly: ```bash -docker run -d -p 8101:8101 -p 40000-40022:40000-40022 \ +docker run -d -p 8101:8101 -p 40000-40023:40000-40023 \ battalion7244/webharbor:latest ``` -Sites are on `40000`-`40022` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: +Sites are on `40000`-`40023` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: | Method | Path | Purpose | |--------|---------------------|-------------------------------------------| @@ -136,13 +136,13 @@ python3 -m py_compile sites//app.py # 3. run on alt ports (don't collide with anything you already have running) docker run -d --rm --name wh-test \ - -p 8201:8101 -p 41000-41022:40000-40022 webharbor:dev + -p 8201:8101 -p 41000-41023:40000-40023 webharbor:dev # 4. control plane healthy, all sites alive curl -s http://localhost:8201/health | python3 -m json.tool | head # 5. every site renders 200 -for p in $(seq 41000 41022); do +for p in $(seq 41000 41023); do curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/ done diff --git a/CLAUDE.md b/CLAUDE.md index 50c5f0db7..12e3cb0fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl ## Existing containers -If a container is already running on `:8101` / `:40000-40022`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41022`). +If a container is already running on `:8101` / `:40000-40023`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41023`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 209076156..5f36384cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ git clone https://github.com//webharbor && cd webharbor ./scripts/fetch_assets.sh # pull current assets ./scripts/new_site.py mywebsite # OR edit an existing site ./scripts/build.sh && docker run -d --rm \ - -p 8101:8101 -p 40000-40022:40000-40022 webharbor:dev + -p 8101:8101 -p 40000-40023:40000-40023 webharbor:dev # iterate locally... ./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out diff --git a/Dockerfile b/Dockerfile index d3fa9c8c7..78f4c5614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 23 Flask mirror sites + control plane on :8101. +# 24 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -66,6 +66,6 @@ os.makedirs('instance_seed', exist_ok=True); \ shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \ print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance -EXPOSE 8101 40000-40022 +EXPOSE 8101 40000-40023 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index 610c7e09f..e93135621 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod - **Deep features unlocked** — carts, checkouts, accounts, all fully testable - **Evolving** — harder tasks drive richer mirrors; the environment grows with agents - **RL-ready** — sub-second database resets between rollouts -- **Community-driven** — 23 sites today, scaling to 100+ together +- **Community-driven** — 24 sites today, scaling to 100+ together ## 🚀 Quickstart One command to run all web environments: ```bash -docker run -p 8101:8101 -p 40000-40022:40000-40022 battalion7244/webharbor:latest +docker run -p 8101:8101 -p 40000-40023:40000-40023 battalion7244/webharbor:latest ``` -Then point your agent at `http://localhost:40000` through `http://localhost:40022` to explore 23 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, and Compass`. +Then point your agent at `http://localhost:40000` through `http://localhost:40023` to explore 24 local mirrors of WebVoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, Ohio State University, Rotten Tomatoes, Compass, and 4shared`. For sub-second reset between rollouts, expose the control plane and call `/reset/`: @@ -65,7 +65,7 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor ## 🤝 Contribute -We have built 23 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. +We have built 24 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. There are two ways to join the author list: diff --git a/control_server.py b/control_server.py index 5602ce02d..7ef5a05a0 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', 'rotten_tomatoes', 'compass', '4shared', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/review-reports/PR-90-FINAL-AUDIT.md b/review-reports/PR-90-FINAL-AUDIT.md new file mode 100644 index 000000000..a6786f8ee --- /dev/null +++ b/review-reports/PR-90-FINAL-AUDIT.md @@ -0,0 +1,89 @@ +# PR #90 final audit — 4shared + +## Result + +**PASS — 20/20 tasks, 252 visible-browser steps, 0 unresolved findings.** + +The review was rerun against the packaged `webharbor:dev` image after all +remediation. Every task started from the configured homepage, used Playwright +visible-element locators, and finished with a persistence check. The site was +reset before and after every task; every reset restored a byte-identical +runtime/seed pair with MD5 `b577adc216900a6f0e3974a80e51c04c`. + +Complete action traces and task screenshots are retained outside the +agent-visible repository to avoid creating answer-bearing benchmark artifacts. +The table below records sanitized endpoints and evidence classes. + +## Per-task review + +| Task | Steps | Screenshot | URL | Issue | Evidence | Impact | Severity | Reproduction | +| --- | ---: | --- | --- | --- | --- | --- | --- | --- | +| 4shared--0 | 15 | `4shared--0-15-reload-persistence-check.png` | `/file/` | None — PASS | Search, category filter, candidate inspection, metadata comparison, reload | Requested identification remained visible | None | Reset → homepage → follow task | +| 4shared--1 | 24 | `4shared--1-24-reload-persistence-check.png` | `/file/` | None — PASS | Images browse, multiple detail inspections, metadata comparison, reload | Requested image evidence remained visible | None | Reset → homepage → follow task | +| 4shared--2 | 7 | `4shared--2-07-reload-persistence-check.png` | `/file/` | None — PASS | Search, category filter, detail inspection, reload | Requested book comparison completed | None | Reset → homepage → follow task | +| 4shared--3 | 15 | `4shared--3-15-reload-persistence-check.png` | `/file/` | None — PASS | Broad search, candidate inspection, detail verification, reload | Multi-clue identification completed | None | Reset → homepage → follow task | +| 4shared--4 | 7 | `4shared--4-07-reload-persistence-check.png` | `/file/` | None — PASS | Broad search, candidate inspection, license/detail verification | Multi-clue identification completed | None | Reset → homepage → follow task | +| 4shared--5 | 6 | `4shared--5-06-reload-persistence-check.png` | `/file/` | None — PASS | Category browse, both detail pages opened, runtimes compared | Cross-item comparison completed | None | Reset → homepage → follow task | +| 4shared--6 | 6 | `4shared--6-06-reload-persistence-check.png` | `/download/` | None — PASS | Six-result search, target at position 6, detail check, download confirmation | Download state changed exactly as requested | None | Reset → homepage → follow task | +| 4shared--7 | 12 | `4shared--7-12-reload-persistence-check.png` | `/favorites` | None — PASS | Login, eight-result search, target at position 6, favorite, reload | Favorite persisted for the requested account | None | Reset → homepage → follow task | +| 4shared--8 | 12 | `4shared--8-12-reload-persistence-check.png` | `/saved` | None — PASS | Login, search, save, Saved files navigation, reload | Saved-file state persisted | None | Reset → homepage → follow task | +| 4shared--9 | 11 | `4shared--9-11-reload-persistence-check.png` | `/account/edit` | None — PASS | Login, profile fields edited, saved, reopened, reloaded | Both account fields persisted | None | Reset → homepage → follow task | +| 4shared--10 | 9 | `4shared--10-09-reload-persistence-check.png` | `/my-files` | None — PASS | Login, root folder creation, reload | Folder persisted at root | None | Reset → homepage → follow task | +| 4shared--11 | 12 | `4shared--11-12-reload-persistence-check.png` | `/my-files?folder=` | None — PASS | Login, upload form, folder/size/description, Documents classification, reload | Private PDF metadata persisted consistently | None | Reset → homepage → follow task | +| 4shared--12 | 12 | `4shared--12-12-reload-persistence-check.png` | `/my-files?folder=` | None — PASS | Login, source folder, rename, move, destination verification, reload | Name and folder changed together | None | Reset → homepage → follow task | +| 4shared--13 | 9 | `4shared--13-09-reload-persistence-check.png` | `/my-files` | None — PASS | Login, Recycle Bin, restore, root verification, reload | Restored file persisted outside Trash | None | Reset → homepage → follow task | +| 4shared--14 | 13 | `4shared--14-13-reload-persistence-check.png` | `/file//share` | None — PASS | Login, private file navigation, label/permission submission, reload | Share-link state persisted | None | Reset → homepage → follow task | +| 4shared--15 | 11 | `4shared--15-11-reload-persistence-check.png` | `/file/` | None — PASS | Login, public search, detail, comment submission, reload | Exact comment persisted | None | Reset → homepage → follow task | +| 4shared--16 | 12 | `4shared--16-12-reload-persistence-check.png` | `/account` | None — PASS | Login, annual 100GB selection, demo checkout, account reload | Plan and storage allowance persisted | None | Reset → homepage → follow task | +| 4shared--17 | 22 | `4shared--17-22-reload-persistence-check.png` | `/file//share` | None — PASS | Folder create, auto-classified PDF upload, rename, preview-only share, reload | All dependent state changes persisted | None | Reset → homepage → follow task | +| 4shared--18 | 21 | `4shared--18-21-reload-persistence-check.png` | `/saved` | None — PASS | Three detail pages compared, login, selected book saved, reload | Comparison and saved state completed | None | Reset → homepage → follow task | +| 4shared--19 | 16 | `4shared--19-16-reload-persistence-check.png` | `/favorites` | None — PASS | Login, broad search, candidate inspection, favorite, download, reload | Both requested mutations persisted | None | Reset → homepage → follow task | + +## Hardening audit + +- **De-leak:** search results expose titles and summary metadata, not decisive + detail facts. Full task trajectories are not committed. Exact-name action + tasks 6 and 7 now have 6 and 8 results respectively, with each target at + position 6. +- **Distractors:** broad searches used by the tasks return 6–40 plausible + candidates. Near matches deliberately differ in detail metadata or package + purpose. +- **Catalog breadth:** 122 public records cover Music, Video, Apps, Images, + Books, Documents, and Archives. All 16 image records use real, locally served + photographs. +- **Cross-field consistency:** filenames, extensions, categories, MIME-facing + behavior, plan names, plan prices, storage allowances, saved-state labels, + and upload classification were checked across list, detail, confirmation, + and account pages. +- **Known leak archetypes:** no prompt-embedded answer, target-count badge, + decisive result-card fact, pre-sorted unique target, first-item target, + insufficient candidate set, direct-route dependency, self-reported-only + completion, visit-only completion, broad mutation, cross-user mutation, + reset drift, or answer-bearing repository artifact remains. + +## Visual and functional validation + +- 51 responsive page checks: 17 representative pages at 1440×900, 390×844, + and 320×720. +- Zero document overflow, broken images, stretched images, out-of-bounds + controls, or unresolved title truncation. +- Seven supplementary flows pass: signed-out upload entry, registration, + re-login, 500GB checkout selection, 1TB checkout selection, three distinct + footer destinations, and explicit public-search scope while authenticated. +- Homepage uses the captured 4shared upload illustration, real mobile-app QR + code/frame, and source store logos. Asset provenance is recorded in + `sites/4shared/ASSET_SOURCES.md`. +- Fresh deterministic seed: 146 files total (122 public), 4 users, 16 folders, + 16 favorites, 12 saved files, 8 downloads, 12 comments, 4 share links, and + 1 plan order. Calling both seed functions twice leaves counts unchanged. + +## PR-safe screenshots + +Only non-answer-bearing homepage screenshots are committed for PR display: + +- `review-reports/assets/pr-90-4shared-homepage-1440.png` +- `review-reports/assets/pr-90-4shared-homepage-390.png` + +The Hugging Face asset PR must merge before `.assets-revision` can be pinned to +its immutable commit. No GitHub or Hugging Face merge is performed by this +review. diff --git a/review-reports/assets/pr-90-4shared-homepage-1440.png b/review-reports/assets/pr-90-4shared-homepage-1440.png new file mode 100644 index 000000000..e66785569 Binary files /dev/null and b/review-reports/assets/pr-90-4shared-homepage-1440.png differ diff --git a/review-reports/assets/pr-90-4shared-homepage-390.png b/review-reports/assets/pr-90-4shared-homepage-390.png new file mode 100644 index 000000000..a43673ddc Binary files /dev/null and b/review-reports/assets/pr-90-4shared-homepage-390.png differ diff --git a/sites/4shared/.requires-images b/sites/4shared/.requires-images new file mode 100644 index 000000000..e69de29bb diff --git a/sites/4shared/ASSET_SOURCES.md b/sites/4shared/ASSET_SOURCES.md new file mode 100644 index 000000000..13035123f --- /dev/null +++ b/sites/4shared/ASSET_SOURCES.md @@ -0,0 +1,54 @@ +# 4shared asset provenance + +All catalog thumbnails are real photographic assets. No generated image, generic +placeholder, or network-loaded runtime image is used. The files live in the +pinned Hugging Face asset bundle because `static/images/` is intentionally +ignored by Git. + +## Existing WebHarbor photographs + +| 4shared path | Existing WebHarbor source path | SHA-256 | +| --- | --- | --- | +| `static/images/london.jpg` | `sites/google_search/static/images/google_real/london.jpg` | `dd11fcb9d34fff87ce03e9008a68adadd0e182c7bfcb7c657fbd608d7b8ef65c` | +| `static/images/new-york.jpg` | `sites/google_search/static/images/google_real/new_york_city.jpg` | `2bb9a4689eb0e3ed5b5c0d654a4db3eb47304ebca2a82f65bd87e8d2898de24b` | +| `static/images/denali.jpg` | `sites/google_search/static/images/google_real/mount_denali_mckinley_elevation.jpg` | `6857da22b8620bd28791d05340eebb5236497b4a1b0bb65452d2a2754215ff5e` | + +## Wikimedia Commons photographs + +The remaining photographs were downloaded as 900-pixel thumbnails from their +Commons file pages, visually checked against the corresponding catalog record, +and converted to optimized JPEGs. + +| Local file | Commons source | Creator | License | +| --- | --- | --- | --- | +| `library-reading-room.jpg` | [Library of Congress main reading room](https://commons.wikimedia.org/wiki/File:INTERIOR,_MAIN_READING_ROOM,_LOOKING_NORTHEAST_-_Library_of_Congress,_Northeast_corner_of_First_Street_and_Independence_Avenue_Southeast,_Washington,_District_of_Columbia,_DC_HABS_DC,WASH,461A-12.tif) | Library of Congress HABS | Public domain | +| `atlantic-boardwalk.jpg` | [Dunes and Boardwalk at Bethany Beach](https://commons.wikimedia.org/wiki/File:Dunes_and_Boardwalk_at_Bethany_Beach,_Delaware.jpg) | PointsofNoReturn | CC BY-SA 4.0 | +| `garden-pollinators.jpg` | [Bee on a blue flower](https://commons.wikimedia.org/wiki/File:Bee-Mating-Blue-Flower-large_ForestWander.jpg) | ForestWander | CC BY-SA 3.0 US | +| `alpine-lake-mist.jpg` | [Sunrise over Shadow Mountain Lake](https://commons.wikimedia.org/wiki/File:Sunrise_over_Shadow_Mountain_Lake,_CO_9-12_(19957710950).jpg) | Don Graham | CC BY-SA 2.0 | +| `ceramic-workbench.jpg` | [Ceramics workshop in Fes](https://commons.wikimedia.org/wiki/File:Inside_of_ceramics_workshop_Fes_Morrocco.jpg) | cliffwilliams | CC BY-SA 2.0 | +| `red-bicycle-brick-wall.jpg` | [Bicycles at a brick wall](https://commons.wikimedia.org/wiki/File:0020-fahrradsammlung-RalfR.jpg) | Ralf Roletschek | Free Art License | +| `winter-pines-snow.jpg` | [Heavy snow on pine branches](https://commons.wikimedia.org/wiki/File:Heavy_snow_on_pine_branches_in_Tuntorp_8.jpg) | W.carter | CC BY-SA 4.0 | +| `notebook-fountain-pen.jpg` | [Pen and notebook](https://commons.wikimedia.org/wiki/File:Pen_and_notebook_-_Narei.jpg) | Kaori Kita | CC BY-SA 3.0 | +| `harbor-boats-fog.jpg` | [Boats in San Francisco fog](https://commons.wikimedia.org/wiki/File:At_San_Francisco_2015_057.jpg) | Mike Peel | CC BY-SA 4.0 | +| `wildflower-trail.jpg` | [Wildflower-lined trail](https://commons.wikimedia.org/wiki/File:Wildflower_lined_trail_(52013518556).jpg) | Joshua Tree National Park | Public domain | +| `classic-camera.jpg` | [Vintage Canon A-1 camera](https://commons.wikimedia.org/wiki/File:Vintage_Canon_35mm_SLR_Camera,_Model_A-1,_All-Digital_Control,_Made_In_Japan,_Circa_1978_(13366931504).jpg) | Joe Haupt | CC BY-SA 2.0 | +| `rainy-window-lights.jpg` | [Rain Drops](https://commons.wikimedia.org/wiki/File:Rain_Drops_-_panoramio.jpg) | M. PINARCI | CC BY-SA 3.0 | +| `map-compass.jpg` | [Suunto compass and map](https://commons.wikimedia.org/wiki/File:Suunto_compass_%26_map_(48995280172).jpg) | Olgierd | CC BY 2.0 | + +## Captured 4shared interface assets + +These public interface assets were harvested from the contributor's sanitized +September 2026 Playwright capture. Only the named public files were copied; no +browser profile, cookies, account state, or private capture material is shipped. + +| Local file | Live source URL | +| --- | --- | +| `static/images/ui/upload-image-initial.svg` | `https://static.4shared.com/images/upload-image-initial.svg` | +| `static/images/ui/qr-code-frame.svg` | `https://static.4shared.com/images/QR-code-frame.svg` | +| `static/images/ui/mob-app-qr-code.svg` | `https://static.4shared.com/images/mob-app-deeplink-QR-code.svg` | +| `static/images/ui/logo-google.svg` | `https://static.4shared.com/images/d1new/Google.svg` | +| `static/images/ui/logo-apple.svg` | `https://static.4shared.com/images/logo-apple-color.svg` | +| `static/images/ui/logo-huawei.svg` | `https://static.4shared.com/images/logo-huawei-color.svg` | + +The repository-native logo mark remains in `static/icons/mark.svg` and is +tracked with the application code. diff --git a/sites/4shared/_health.py b/sites/4shared/_health.py new file mode 100644 index 000000000..f33f8ddc8 --- /dev/null +++ b/sites/4shared/_health.py @@ -0,0 +1,3 @@ +"""Per-site health probe (optional, called by control_server).""" +def health(): + return {"ok": True, "site": "4shared"} diff --git a/sites/4shared/app.py b/sites/4shared/app.py new file mode 100644 index 000000000..2b8a5d603 --- /dev/null +++ b/sites/4shared/app.py @@ -0,0 +1,720 @@ +"""4shared mirror for the WebHarbor offline benchmark.""" + +from __future__ import annotations + +import hashlib +import os +import re +import secrets +from datetime import datetime +from pathlib import Path +from urllib.parse import urlparse + +from flask import Flask, abort, flash, redirect, render_template, request, url_for +from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user +from flask_sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CSRFProtect +from sqlalchemy import event +from sqlalchemy.engine import Engine + + +SITE_SLUG = "4shared" +SITE_NAME = "4shared" +SITE_PORT = 40023 +BENCHMARK_PASSWORD = "TestPass123!" +BASE_DIR = Path(__file__).resolve().parent +INSTANCE_DIR = BASE_DIR / "instance" +SEED_DIR = BASE_DIR / "instance_seed" +RUNTIME_DB_PATH = INSTANCE_DIR / "4shared.db" +SEED_DB_PATH = SEED_DIR / "4shared.db" +PASSWORD_NAMESPACE = "webharbor-4shared-v1" + +PREMIUM_PLANS = { + "100": {"label": "Premium 100 GB", "account_plan": "Premium", "storage_mb": 102400, "annual": 77.88}, + "500": {"label": "Premium 500 GB", "account_plan": "Premium 500 GB", "storage_mb": 512000, "annual": 29.99}, + "1000": {"label": "Premium 1 TB", "account_plan": "Premium 1 TB", "storage_mb": 1048576, "annual": 39.99}, +} + +UPLOAD_CATEGORY_BY_EXTENSION = { + "aac": "Music", "flac": "Music", "m4a": "Music", "mp3": "Music", "ogg": "Music", "wav": "Music", + "avi": "Video", "mkv": "Video", "mov": "Video", "mp4": "Video", "webm": "Video", + "gif": "Images", "jpeg": "Images", "jpg": "Images", "png": "Images", "webp": "Images", + "epub": "Books", "mobi": "Books", + "7z": "Archives", "rar": "Archives", "tar": "Archives", "zip": "Archives", + "apk": "Apps", "dmg": "Apps", "exe": "Apps", "msi": "Apps", +} + +INSTANCE_DIR.mkdir(parents=True, exist_ok=True) +SEED_DIR.mkdir(parents=True, exist_ok=True) + +app = Flask(__name__, instance_path=str(INSTANCE_DIR)) +app.config.update( + SECRET_KEY="webharbor-4shared-deterministic-development-key", + SQLALCHEMY_DATABASE_URI=f"sqlite:///{RUNTIME_DB_PATH}", + SQLALCHEMY_TRACK_MODIFICATIONS=False, + MAX_CONTENT_LENGTH=4 * 1024 * 1024, +) +db = SQLAlchemy(app) +csrf = CSRFProtect(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message = "Log in to manage files and folders." + + +@event.listens_for(Engine, "connect") +def enable_sqlite_foreign_keys(dbapi_connection, _connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + +def stable_password_hash(password: str) -> str: + return hashlib.sha256(f"{PASSWORD_NAMESPACE}:{password}".encode()).hexdigest() + + +def slugify(value: str) -> str: + value = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") + return value or "file" + + +def safe_next(target: str | None, fallback: str) -> str: + if not target or "\\" in target: + return fallback + parsed = urlparse(target) + if parsed.scheme or parsed.netloc or not target.startswith("/") or target.startswith("//"): + return fallback + return target + + +def human_size(value: int) -> str: + size = float(value) + for unit in ("B", "KB", "MB", "GB"): + if size < 1024 or unit == "GB": + return f"{size:.1f} {unit}" if unit != "B" else f"{int(size)} B" + size /= 1024 + return f"{size:.1f} GB" + + +app.jinja_env.filters["filesize"] = human_size + + +class User(db.Model, UserMixin): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(160), unique=True, nullable=False, index=True) + display_name = db.Column(db.String(120), nullable=False) + password_hash = db.Column(db.String(64), nullable=False) + location = db.Column(db.String(120), default="") + bio = db.Column(db.Text, default="") + plan = db.Column(db.String(32), default="Free") + storage_limit_mb = db.Column(db.Integer, default=15360) + joined_at = db.Column(db.DateTime, nullable=False) + + def set_password(self, password: str) -> None: + self.password_hash = stable_password_hash(password) + + def check_password(self, password: str) -> bool: + return secrets.compare_digest(self.password_hash, stable_password_hash(password)) + + @property + def storage_used(self) -> int: + return sum(item.size_bytes for item in self.files if not item.deleted) + + +class Folder(db.Model): + __tablename__ = "folders" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + name = db.Column(db.String(120), nullable=False) + parent_id = db.Column(db.Integer, db.ForeignKey("folders.id", ondelete="CASCADE")) + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref=db.backref("folders", cascade="all, delete-orphan"), foreign_keys=[user_id]) + parent = db.relationship("Folder", remote_side=[id], backref="children") + __table_args__ = (db.UniqueConstraint("user_id", "parent_id", "name", name="uq_folder_parent_name"),) + + +class FileItem(db.Model): + __tablename__ = "files" + id = db.Column(db.Integer, primary_key=True) + owner_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), index=True) + folder_id = db.Column(db.Integer, db.ForeignKey("folders.id", ondelete="SET NULL"), index=True) + filename = db.Column(db.String(220), nullable=False) + slug = db.Column(db.String(260), unique=True, nullable=False, index=True) + category = db.Column(db.String(32), nullable=False, index=True) + extension = db.Column(db.String(12), nullable=False) + mime_type = db.Column(db.String(100), nullable=False) + size_bytes = db.Column(db.Integer, nullable=False) + description = db.Column(db.Text, default="") + tags = db.Column(db.String(400), default="") + license_name = db.Column(db.String(100), default="") + uploader_name = db.Column(db.String(120), nullable=False) + public = db.Column(db.Boolean, default=True, nullable=False, index=True) + featured = db.Column(db.Boolean, default=False, nullable=False) + deleted = db.Column(db.Boolean, default=False, nullable=False) + thumbnail = db.Column(db.String(240), default="") + preview_text = db.Column(db.Text, default="") + uploaded_at = db.Column(db.DateTime, nullable=False, index=True) + modified_at = db.Column(db.DateTime, nullable=False) + download_count = db.Column(db.Integer, default=0, nullable=False) + rating = db.Column(db.Float, default=4.5, nullable=False) + + owner = db.relationship("User", backref=db.backref("files", cascade="all, delete-orphan")) + folder = db.relationship("Folder", backref="files") + + @property + def stem(self) -> str: + return self.filename.rsplit(".", 1)[0] + + +class Favorite(db.Model): + __tablename__ = "favorites" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + file_id = db.Column(db.Integer, db.ForeignKey("files.id", ondelete="CASCADE"), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref=db.backref("favorites", cascade="all, delete-orphan")) + file = db.relationship("FileItem", backref=db.backref("favorite_rows", cascade="all, delete-orphan")) + __table_args__ = (db.UniqueConstraint("user_id", "file_id", name="uq_favorite_user_file"),) + + +class SavedFile(db.Model): + __tablename__ = "saved_files" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + file_id = db.Column(db.Integer, db.ForeignKey("files.id", ondelete="CASCADE"), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref=db.backref("saved_files", cascade="all, delete-orphan")) + file = db.relationship("FileItem") + __table_args__ = (db.UniqueConstraint("user_id", "file_id", name="uq_saved_user_file"),) + + +class DownloadLog(db.Model): + __tablename__ = "downloads" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) + file_id = db.Column(db.Integer, db.ForeignKey("files.id", ondelete="CASCADE"), nullable=False) + downloaded_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref="downloads") + file = db.relationship("FileItem", backref="download_rows") + + +class SharedLink(db.Model): + __tablename__ = "shared_links" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + file_id = db.Column(db.Integer, db.ForeignKey("files.id", ondelete="CASCADE"), nullable=False) + token = db.Column(db.String(48), unique=True, nullable=False, index=True) + permission = db.Column(db.String(24), default="view") + label = db.Column(db.String(120), default="") + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref=db.backref("shared_links", cascade="all, delete-orphan")) + file = db.relationship("FileItem", backref="share_links") + + +class Comment(db.Model): + __tablename__ = "comments" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + file_id = db.Column(db.Integer, db.ForeignKey("files.id", ondelete="CASCADE"), nullable=False) + body = db.Column(db.String(600), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref="comments") + file = db.relationship("FileItem", backref=db.backref("comments", order_by="Comment.created_at.desc()")) + + +class PlanOrder(db.Model): + __tablename__ = "plan_orders" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + plan_name = db.Column(db.String(40), nullable=False) + billing_period = db.Column(db.String(24), nullable=False) + amount = db.Column(db.Float, nullable=False) + card_last4 = db.Column(db.String(4), nullable=False) + status = db.Column(db.String(24), default="Active", nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + user = db.relationship("User", backref="plan_orders") + + +@login_manager.user_loader +def load_user(user_id: str): + return db.session.get(User, int(user_id)) + + +def public_files_query(): + return FileItem.query.filter_by(public=True, deleted=False) + + +STOP_WORDS = {"the", "a", "an", "in", "on", "at", "to", "for", "of", "and", "or", "is", "it", "by", "with"} + + +def scored_search(query: str, items: list[FileItem]) -> list[FileItem]: + tokens = [token for token in re.split(r"\W+", query.lower()) if len(token) > 1 and token not in STOP_WORDS] + if not tokens: + return items + ranked = [] + for item in items: + title = item.filename.lower() + blob = " ".join((item.filename, item.description, item.tags, item.category, item.extension, item.uploader_name)).lower() + score = sum(4 if token in title else 1 for token in tokens if token in blob) + if score: + ranked.append((item, score)) + ranked.sort(key=lambda row: (-row[1], -row[0].download_count, row[0].filename.lower())) + return [item for item, _score in ranked] + + +def owned_file_or_404(file_id: int) -> FileItem: + item = db.session.get(FileItem, file_id) + if not item or item.owner_id != current_user.id: + abort(404) + return item + + +@app.context_processor +def common_context(): + categories = ["Music", "Video", "Apps", "Images", "Books", "Documents", "Archives"] + favorite_ids = set() + if current_user.is_authenticated: + favorite_ids = {row.file_id for row in current_user.favorites} + return {"nav_categories": categories, "favorite_ids": favorite_ids, "site_port": SITE_PORT} + + +@app.route("/") +def index(): + featured = public_files_query().filter_by(featured=True).order_by(FileItem.id.asc()).limit(8).all() + popular = public_files_query().order_by(FileItem.download_count.desc(), FileItem.id.asc()).limit(8).all() + recent = public_files_query().order_by(FileItem.uploaded_at.desc(), FileItem.id.asc()).limit(8).all() + return render_template("index.html", featured=featured, popular=popular, recent=recent) + + +@app.route("/search") +def search(): + query = request.args.get("q", "").strip()[:120] + category_name = request.args.get("category", "All Files").strip()[:32] + sort = request.args.get("sort", "relevance") + items = public_files_query().all() + if category_name and category_name != "All Files": + items = [item for item in items if item.category.lower() == category_name.lower()] + items = scored_search(query, items) + if sort == "downloads": + items.sort(key=lambda item: (-item.download_count, item.filename.lower())) + elif sort == "newest": + items.sort(key=lambda item: (-item.uploaded_at.timestamp(), item.filename.lower())) + elif sort == "size": + items.sort(key=lambda item: (-item.size_bytes, item.filename.lower())) + return render_template("search.html", files=items, query=query, category=category_name, sort=sort) + + +@app.route("/category/") +def category(category: str): + display = category.replace("-", " ").title() + files = public_files_query().filter(db.func.lower(FileItem.category) == display.lower()).order_by(FileItem.download_count.desc()).all() + if not files: + abort(404) + return render_template("category.html", files=files, category=display) + + +@app.route("/file/") +def file_detail(slug: str): + item = FileItem.query.filter_by(slug=slug, deleted=False).first_or_404() + if not item.public and (not current_user.is_authenticated or item.owner_id != current_user.id): + abort(404) + related = public_files_query().filter(FileItem.category == item.category, FileItem.id != item.id).order_by(FileItem.download_count.desc()).limit(6).all() + return render_template("file_detail.html", file=item, related=related) + + +@app.route("/preview/") +def preview(file_id: int): + item = db.get_or_404(FileItem, file_id) + if item.deleted or (not item.public and (not current_user.is_authenticated or item.owner_id != current_user.id)): + abort(404) + return render_template("preview.html", file=item) + + +@app.post("/download/") +def download(file_id: int): + item = db.get_or_404(FileItem, file_id) + if item.deleted or (not item.public and (not current_user.is_authenticated or item.owner_id != current_user.id)): + abort(404) + item.download_count += 1 + db.session.add(DownloadLog(user_id=current_user.id if current_user.is_authenticated else None, file_id=item.id, downloaded_at=datetime.utcnow())) + db.session.commit() + return render_template("download_ready.html", file=item) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").strip().lower()[:160] + password = request.form.get("password", "") + user = User.query.filter_by(email=email).first() + if user and user.check_password(password): + login_user(user) + return redirect(safe_next(request.args.get("next"), url_for("account"))) + flash("The email or password is incorrect.", "error") + return render_template("login.html") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if request.method == "POST": + display_name = request.form.get("display_name", "").strip()[:120] + email = request.form.get("email", "").strip().lower()[:160] + password = request.form.get("password", "") + if len(display_name) < 2 or "@" not in email or len(password) < 8: + flash("Enter a name, valid email, and password of at least 8 characters.", "error") + elif User.query.filter_by(email=email).first(): + flash("An account with that email already exists.", "error") + else: + user = User(email=email, display_name=display_name, joined_at=datetime.utcnow()) + user.set_password(password) + db.session.add(user) + db.session.commit() + login_user(user) + flash("Welcome to 4shared. Your account is ready.", "success") + return redirect(url_for("account")) + return render_template("register.html") + + +@app.get("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("index")) + + +@app.get("/account") +@login_required +def account(): + recent_files = FileItem.query.filter_by(owner_id=current_user.id, deleted=False).order_by(FileItem.modified_at.desc()).limit(6).all() + recent_downloads = DownloadLog.query.filter_by(user_id=current_user.id).order_by(DownloadLog.downloaded_at.desc()).limit(6).all() + return render_template("account.html", recent_files=recent_files, recent_downloads=recent_downloads) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + if request.method == "POST": + name = request.form.get("display_name", "").strip()[:120] + if len(name) < 2: + flash("Display name must have at least two characters.", "error") + else: + current_user.display_name = name + current_user.location = request.form.get("location", "").strip()[:120] + current_user.bio = request.form.get("bio", "").strip()[:500] + db.session.commit() + flash("Profile updated.", "success") + return redirect(url_for("account")) + return render_template("account_edit.html") + + +@app.get("/my-files") +@login_required +def my_files(): + folder_id = request.args.get("folder", type=int) + active_folder = None + if folder_id: + active_folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first_or_404() + folders = Folder.query.filter_by(user_id=current_user.id, parent_id=folder_id).order_by(Folder.name).all() + files = FileItem.query.filter_by(owner_id=current_user.id, folder_id=folder_id, deleted=False).order_by(FileItem.filename).all() + all_folders = Folder.query.filter_by(user_id=current_user.id).order_by(Folder.name).all() + return render_template("my_files.html", folders=folders, files=files, active_folder=active_folder, all_folders=all_folders) + + +@app.post("/folder/new") +@login_required +def folder_new(): + name = request.form.get("name", "").strip()[:120] + parent_id = request.form.get("parent_id", type=int) + if parent_id and not Folder.query.filter_by(id=parent_id, user_id=current_user.id).first(): + abort(404) + if not name: + flash("Folder name is required.", "error") + elif Folder.query.filter_by(user_id=current_user.id, parent_id=parent_id, name=name).first(): + flash("A folder with that name already exists here.", "error") + else: + db.session.add(Folder(user_id=current_user.id, parent_id=parent_id, name=name, created_at=datetime.utcnow())) + db.session.commit() + flash(f"Folder ‘{name}’ created.", "success") + return redirect(url_for("my_files", folder=parent_id) if parent_id else url_for("my_files")) + + +@app.route("/upload", methods=["GET", "POST"]) +@login_required +def upload(): + folders = Folder.query.filter_by(user_id=current_user.id).order_by(Folder.name).all() + if request.method == "POST": + filename = request.form.get("filename", "").strip()[:220] + category_name = request.form.get("category", "auto")[:32] + description = request.form.get("description", "").strip()[:1000] + folder_id = request.form.get("folder_id", type=int) + if folder_id and not Folder.query.filter_by(id=folder_id, user_id=current_user.id).first(): + abort(404) + if not filename or "." not in filename: + flash("Enter a filename with an extension, such as notes.pdf.", "error") + else: + extension = filename.rsplit(".", 1)[1].lower()[:12] + if category_name == "auto": + category_name = UPLOAD_CATEGORY_BY_EXTENSION.get(extension, "Documents") + elif category_name not in {"Music", "Video", "Apps", "Images", "Books", "Documents", "Archives"}: + category_name = "Documents" + slug = f"{slugify(filename)}-{current_user.id}-{int(datetime.utcnow().timestamp())}" + item = FileItem( + owner_id=current_user.id, folder_id=folder_id, filename=filename, slug=slug, + category=category_name, extension=extension, mime_type="application/octet-stream", + size_bytes=max(1024, request.form.get("size_kb", type=int, default=128) * 1024), + description=description, tags="personal upload", license_name="Private", + uploader_name=current_user.display_name, public=request.form.get("public") == "on", + uploaded_at=datetime.utcnow(), modified_at=datetime.utcnow(), preview_text=description, + ) + db.session.add(item) + db.session.commit() + flash(f"{filename} uploaded.", "success") + return redirect(url_for("my_files", folder=folder_id) if folder_id else url_for("my_files")) + return render_template("upload.html", folders=folders) + + +@app.post("/file//rename") +@login_required +def rename_file(file_id: int): + item = owned_file_or_404(file_id) + filename = request.form.get("filename", "").strip()[:220] + if not filename or "." not in filename: + flash("Enter a complete filename.", "error") + else: + item.filename = filename + item.extension = filename.rsplit(".", 1)[1].lower()[:12] + item.modified_at = datetime.utcnow() + db.session.commit() + flash("File renamed.", "success") + return redirect(url_for("my_files", folder=item.folder_id) if item.folder_id else url_for("my_files")) + + +@app.post("/file//move") +@login_required +def move_file(file_id: int): + item = owned_file_or_404(file_id) + folder_id = request.form.get("folder_id", type=int) + if folder_id and not Folder.query.filter_by(id=folder_id, user_id=current_user.id).first(): + abort(404) + item.folder_id = folder_id + item.modified_at = datetime.utcnow() + db.session.commit() + flash("File moved.", "success") + return redirect(url_for("my_files", folder=folder_id) if folder_id else url_for("my_files")) + + +@app.post("/file//delete") +@login_required +def delete_file(file_id: int): + item = owned_file_or_404(file_id) + item.deleted = True + item.modified_at = datetime.utcnow() + db.session.commit() + flash("File moved to Trash.", "success") + return redirect(url_for("my_files")) + + +@app.get("/trash") +@login_required +def trash(): + files = FileItem.query.filter_by(owner_id=current_user.id, deleted=True).order_by(FileItem.modified_at.desc()).all() + return render_template("trash.html", files=files) + + +@app.post("/file//restore") +@login_required +def restore_file(file_id: int): + item = owned_file_or_404(file_id) + item.deleted = False + item.modified_at = datetime.utcnow() + db.session.commit() + flash("File restored.", "success") + return redirect(url_for("trash")) + + +@app.post("/file//favorite") +@login_required +def toggle_favorite(file_id: int): + item = db.get_or_404(FileItem, file_id) + if item.deleted: + abort(404) + row = Favorite.query.filter_by(user_id=current_user.id, file_id=item.id).first() + if row: + db.session.delete(row) + flash("Removed from favorites.", "success") + else: + db.session.add(Favorite(user_id=current_user.id, file_id=item.id, created_at=datetime.utcnow())) + flash("Added to favorites.", "success") + db.session.commit() + return redirect(safe_next(request.form.get("next"), url_for("file_detail", slug=item.slug))) + + +@app.get("/favorites") +@login_required +def favorites(): + rows = Favorite.query.filter_by(user_id=current_user.id).order_by(Favorite.created_at.desc()).all() + return render_template("favorites.html", files=[row.file for row in rows if not row.file.deleted]) + + +@app.post("/file//save") +@login_required +def save_file(file_id: int): + item = db.get_or_404(FileItem, file_id) + if not item.public or item.deleted: + abort(404) + if not SavedFile.query.filter_by(user_id=current_user.id, file_id=item.id).first(): + db.session.add(SavedFile(user_id=current_user.id, file_id=item.id, created_at=datetime.utcnow())) + db.session.commit() + flash("Saved to My 4shared.", "success") + return redirect(url_for("file_detail", slug=item.slug)) + + +@app.get("/saved") +@login_required +def saved(): + rows = SavedFile.query.filter_by(user_id=current_user.id).order_by(SavedFile.created_at.desc()).all() + return render_template("favorites.html", files=[row.file for row in rows], title="Saved files") + + +@app.route("/file//share", methods=["GET", "POST"]) +@login_required +def share_file(file_id: int): + item = db.get_or_404(FileItem, file_id) + if not item.public and item.owner_id != current_user.id: + abort(404) + if request.method == "POST": + permission = request.form.get("permission", "view") + if permission not in {"view", "download"}: + permission = "view" + token = secrets.token_urlsafe(12) + link = SharedLink(user_id=current_user.id, file_id=item.id, token=token, permission=permission, + label=request.form.get("label", "").strip()[:120], created_at=datetime.utcnow()) + db.session.add(link) + db.session.commit() + flash("Share link created.", "success") + return redirect(url_for("share_file", file_id=item.id)) + links = SharedLink.query.filter_by(user_id=current_user.id, file_id=item.id).order_by(SharedLink.created_at.desc()).all() + return render_template("share.html", file=item, links=links) + + +@app.get("/shared/") +def shared(token: str): + link = SharedLink.query.filter_by(token=token).first_or_404() + if link.file.deleted: + abort(404) + return render_template("shared.html", link=link, file=link.file) + + +@app.post("/file//comment") +@login_required +def add_comment(file_id: int): + item = db.get_or_404(FileItem, file_id) + body = request.form.get("body", "").strip()[:600] + if len(body) < 2: + flash("Comment cannot be empty.", "error") + else: + db.session.add(Comment(user_id=current_user.id, file_id=item.id, body=body, created_at=datetime.utcnow())) + db.session.commit() + flash("Comment posted.", "success") + return redirect(url_for("file_detail", slug=item.slug)) + + +@app.get("/activity") +@login_required +def activity(): + downloads = DownloadLog.query.filter_by(user_id=current_user.id).order_by(DownloadLog.downloaded_at.desc()).all() + shares = SharedLink.query.filter_by(user_id=current_user.id).order_by(SharedLink.created_at.desc()).all() + return render_template("activity.html", downloads=downloads, shares=shares) + + +@app.get("/premium") +def premium(): + return render_template("premium.html") + + +@app.route("/premium/checkout", methods=["GET", "POST"]) +@login_required +def premium_checkout(): + plan_key = request.values.get("plan", "100") + if plan_key not in PREMIUM_PLANS: + plan_key = "100" + plan = PREMIUM_PLANS[plan_key] + period = "annual" + amount = plan[period] + if request.method == "POST": + card = re.sub(r"\D", "", request.form.get("card_number", "")) + holder = request.form.get("cardholder", "").strip() + if len(card) != 16 or len(holder) < 2: + flash("Enter the demo 16-digit card number and cardholder name.", "error") + else: + order = PlanOrder(user_id=current_user.id, plan_name=plan["label"], billing_period=period, + amount=amount, card_last4=card[-4:], created_at=datetime.utcnow()) + current_user.plan = plan["account_plan"] + current_user.storage_limit_mb = plan["storage_mb"] + db.session.add(order) + db.session.commit() + return render_template("premium_confirmed.html", order=order, plan=plan) + return render_template("premium_checkout.html", period=period, amount=amount, plan=plan, plan_key=plan_key) + + +@app.get("/help") +def help_center(): + return render_template("help.html") + + +@app.get("/about") +def about(): + return render_template("about.html") + + +@app.get("/press-room") +def press_room(): + return render_template("press_room.html") + + +@app.get("/blog") +def blog(): + return render_template("blog.html") + + +@app.route("/convert/-to-pdf", methods=["GET", "POST"]) +def convert_to_pdf(source_format: str): + allowed_formats = {"doc", "pptx", "docx", "xls", "ppt", "xlsx", "cbr", "txt", "pps", "rtf", "cbz", "fb2", "epub", "djvu"} + if source_format not in allowed_formats: + abort(404) + converted_name = None + if request.method == "POST": + filename = request.form.get("filename", "").strip()[:220] + if not filename.lower().endswith(f".{source_format}"): + flash(f"Choose a .{source_format} file record to convert.", "error") + else: + converted_name = f"{filename.rsplit('.', 1)[0]}.pdf" + return render_template("converter.html", source_format=source_format, converted_name=converted_name) + + +@app.get("/_health") +def health(): + return {"ok": True, "site": SITE_SLUG, "files": public_files_query().count()} + + +@app.errorhandler(404) +def not_found(_error): + return render_template("404.html"), 404 + + +def initialize_database() -> None: + with app.app_context(): + db.create_all() + from seed_data import seed_benchmark_users, seed_database + seed_database() + seed_benchmark_users() + + +initialize_database() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", SITE_PORT)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/4shared/requirements.txt b/sites/4shared/requirements.txt new file mode 100644 index 000000000..cde776e2a --- /dev/null +++ b/sites/4shared/requirements.txt @@ -0,0 +1,6 @@ +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +Flask-WTF==1.2.2 +SQLAlchemy==2.0.36 +Werkzeug==3.1.3 diff --git a/sites/4shared/seed_data.py b/sites/4shared/seed_data.py new file mode 100644 index 000000000..acbdb8abe --- /dev/null +++ b/sites/4shared/seed_data.py @@ -0,0 +1,381 @@ +"""Deterministic seed data for the 4shared WebHarbor mirror.""" + +from __future__ import annotations + +import sys +from datetime import datetime, timedelta + + +def _app_module(): + module = sys.modules.get("app") + if module is not None: + return module + main = sys.modules.get("__main__") + if main is not None and hasattr(main, "db") and hasattr(main, "FileItem"): + return main + import app as module + return module + + +_app = _app_module() +BENCHMARK_PASSWORD = _app.BENCHMARK_PASSWORD +Comment = _app.Comment +DownloadLog = _app.DownloadLog +Favorite = _app.Favorite +FileItem = _app.FileItem +Folder = _app.Folder +PlanOrder = _app.PlanOrder +SavedFile = _app.SavedFile +SharedLink = _app.SharedLink +User = _app.User +db = _app.db +slugify = _app.slugify + +SEED_TIME = datetime(2026, 8, 20, 10, 0, 0) + +UPLOADERS = [ + "Open Culture Shelf", "Atlas Media Lab", "Commons Studio", "Learning Exchange", + "Field Notes Collective", "Open Source Desk", "Archive Lantern", "Community Library", +] + +CATALOG = { + "Music": { + "ext": "mp3", "mime": "audio/mpeg", "license": "Public domain recording", + "items": [ + ("Moonlight Sonata First Movement", "A measured solo-piano performance recorded in a quiet recital hall.", "piano beethoven classical nocturne", "Duration 5:42 · 320 kbps · recorded on a Steinway Model B."), + ("Clair de Lune Studio Performance", "An intimate interpretation of Debussy's atmospheric piano work.", "piano debussy impressionist classical", "Duration 4:51 · 256 kbps · remastered from a 2018 session."), + ("Morning Meadow Field Recording", "Dawn birds, light wind, and a distant creek captured in early spring.", "nature birds ambience field recording", "Duration 12:08 · stereo · recorded at 48 kHz."), + ("Nocturne in E Flat Practice Take", "A complete practice-room reading with natural room ambience.", "chopin piano nocturne practice", "Duration 4:33 · 192 kbps · take number 7."), + ("Cello Suite Prelude Live", "A warm live performance of the familiar unaccompanied prelude.", "bach cello suite live classical", "Duration 3:09 · audience recording · restored in 2024."), + ("Rain on Library Windows", "A seamless ambience track of gentle rain against tall windows.", "rain ambience sleep study nature", "Duration 18:40 · stereo · no music or voice."), + ("Blue Hour Jazz Trio", "Original piano, upright bass, and brushed drums in a relaxed medium tempo.", "jazz trio original instrumental", "Duration 6:17 · 24-bit source · key of F minor."), + ("Acoustic Guitar Warmup Etude", "A fingerstyle study designed for intermediate practice sessions.", "guitar acoustic etude practice", "Duration 2:58 · 120 BPM · standard tuning."), + ("Ocean Pier Evening Ambience", "Waves, gulls, and wooden pier creaks recorded just after sunset.", "ocean waves ambience coast", "Duration 14:22 · binaural recording · light wind."), + ("Brass Quintet Festival Fanfare", "An original ceremonial fanfare for two trumpets, horn, trombone, and tuba.", "brass quintet fanfare original", "Duration 2:14 · score revision 3 · concert pitch."), + ("Violin Partita Courante", "A clear rehearsal recording focused on articulation and dance rhythm.", "violin bach partita baroque", "Duration 3:37 · mono room mic · no edits."), + ("Quiet Cafe Background Loop", "Low conversation and table sounds for creative-work ambience.", "cafe ambience background focus", "Duration 9:30 · seamless loop · no identifiable speech."), + ("Mountain Stream in Late Summer", "Close-miked flowing water from a shaded alpine stream.", "water stream nature field recording", "Duration 10:45 · 48 kHz WAV source · normalized to -16 LUFS."), + ("String Quartet Rehearsal Sketch", "An original two-theme chamber sketch from an open rehearsal.", "strings quartet rehearsal original", "Duration 7:06 · rehearsal letter C begins at 3:12."), + ("Vintage Metronome at 72 BPM", "A wooden mechanical metronome captured for music practice.", "metronome rhythm practice 72 bpm", "Duration 5:00 · 72 beats per minute · dry studio sound."), + ("Evening Crickets Field Session", "A summer-night chorus recorded near a woodland edge.", "crickets night nature ambience", "Duration 11:11 · stereo pair · recorded in August."), + ], + }, + "Video": { + "ext": "mp4", "mime": "video/mp4", "license": "Creative Commons Attribution", + "items": [ + ("Introduction to Urban Sketching", "A practical lesson on line, shape, and quick watercolor washes.", "art drawing watercolor tutorial", "Runtime 18:24 · 1080p · includes three street-scene demonstrations."), + ("Night Walk Across Tower Bridge", "A stabilized dusk-to-night walk with ambient city sound.", "london travel bridge city walk", "Runtime 12:36 · 4K master · filmed from south to north."), + ("Denali Landscape Study", "A slow visual study of the mountain, tundra, and reflected light.", "denali alaska mountain nature landscape", "Runtime 8:15 · 2160p · filmed over two clear mornings."), + ("Build a Simple Weather Station", "A classroom demonstration using open hardware sensors.", "science weather tutorial sensors education", "Runtime 22:41 · 1080p · bill of materials appears at 04:18."), + ("Five-Minute Desk Mobility Routine", "A low-impact guided routine for shoulders, hips, and wrists.", "fitness mobility desk stretch", "Runtime 5:38 · captions included · no equipment required."), + ("Open Data Mapping Basics", "A beginner overview of layers, coordinates, and map styling.", "maps gis open data tutorial", "Runtime 27:03 · 1080p · sample project uses GeoJSON."), + ("Coastal Birds Field Guide", "Identification notes and footage for eight common shoreline birds.", "birds coast nature guide", "Runtime 14:19 · captions included · eight species chapters."), + ("Bread Dough Fermentation Timelapse", "A controlled side-by-side rise at three room temperatures.", "bread baking science timelapse", "Runtime 6:52 · labels show 18°C, 22°C, and 27°C."), + ("Community Garden Summer Tour", "A volunteer-led walk through pollinator beds and raised plots.", "garden plants community tour", "Runtime 16:08 · 1080p · filmed in July."), + ("Beginner Astronomy Moon Phases", "A model-based explanation of the lunar cycle and viewing geometry.", "astronomy moon phases education", "Runtime 11:44 · includes a 29.5-day cycle diagram."), + ("Restoring a Wooden Chair", "A careful repair demonstration from disassembly through finish.", "woodworking restoration chair tutorial", "Runtime 31:20 · chapter markers · hand tools only."), + ("Museum Archive Handling Basics", "Gloves, supports, labeling, and safe movement of paper objects.", "museum archive preservation training", "Runtime 13:57 · accessibility captions · revised 2025."), + ("City Cycling Route Planning", "How to evaluate gradients, protected lanes, and intersection risk.", "cycling city maps route planning", "Runtime 19:05 · sample route length 8.4 km."), + ("Watercolor Clouds Three Techniques", "Wet-on-wet, lifting, and dry-brush cloud studies.", "painting watercolor clouds art", "Runtime 15:31 · 1080p · materials list in opening minute."), + ("Library Digitization Workflow", "A demonstration of capture, naming, metadata, and quality control.", "library scanning metadata workflow", "Runtime 24:12 · TIFF master workflow · PDF access copies."), + ("Seed Saving for Beginners", "A seasonal guide to collecting, drying, labeling, and storage.", "garden seeds sustainability guide", "Runtime 17:46 · covers tomatoes, beans, and lettuce."), + ], + }, + "Apps": { + "ext": "zip", "mime": "application/zip", "license": "Open-source package", + "items": [ + ("OpenMap Desktop Portable", "Portable offline map viewer package with sample public data.", "maps desktop offline open source", "Version 3.4.2 · Linux and Windows launchers · SHA-256 listed in README."), + ("NoteStack Markdown Editor", "A lightweight local-first editor for Markdown notes.", "notes markdown editor productivity", "Version 2.8.0 · spellcheck included · export to HTML and PDF."), + ("PhotoBatch Community Edition", "Resize, rotate, and rename image collections without cloud upload.", "photos images batch resize open source", "Version 1.9.5 · supports JPEG, PNG, and WebP."), + ("AudioTag Library Tool", "Edit common audio metadata fields and organize albums.", "music audio tags organizer", "Version 4.1.1 · reads ID3 and Vorbis comments."), + ("StudyTimer Focus Utility", "A simple configurable focus and break timer.", "timer study focus productivity", "Version 1.6.3 · three color themes · CSV session export."), + ("ArchivePeek File Inspector", "Browse archive contents and checksums before extraction.", "archive zip checksum utility", "Version 2.2.4 · ZIP, TAR, and 7z read support."), + ("ArchivePeek File Inspector Legacy Build", "An archived compatibility build of the file inspector for older systems.", "archive file inspector legacy compatibility", "Version 1.7.9 · ZIP-only inspection · no checksum comparison."), + ("ArchivePeek File Inspector Checksums Add-on", "Optional checksum definitions for ArchivePeek deployments.", "archive file inspector checksum addon", "Version 2.1.0 · definitions package only · requires the main application."), + ("ArchivePeek File Inspector Portable Notes", "Release notes and deployment examples for portable ArchivePeek installations.", "archive file inspector portable documentation", "Version 2.2 notes · documentation package · contains no executable."), + ("ArchivePeek File Inspector Recovery Plug-in", "A recovery plug-in for damaged archive headers.", "archive file inspector recovery plugin", "Version 0.6.3 · experimental plug-in · TAR recovery only."), + ("ArchivePeek File Inspector Test Fixtures", "Sample archives for validating file-inspection workflows.", "archive file inspector test fixtures", "Version 2026.4 · 42 synthetic fixtures · not an application installer."), + ("ColorScope Palette Assistant", "Inspect colors and create accessible palette combinations.", "design color accessibility palette", "Version 5.0.0 · WCAG contrast preview · GPL-3.0."), + ("PocketWeather Sample Client", "Demonstration client for an open weather-data endpoint.", "weather sample api client", "Version 0.9.8 · demo data works offline · MIT license."), + ("BookShelf EPUB Catalog", "Catalog local EPUB metadata and reading status.", "books epub catalog library", "Version 3.0.1 · OPF metadata import · local database only."), + ("SubtitleShift Timing Utility", "Adjust subtitle timing by a fixed offset or scale.", "video subtitles timing utility", "Version 1.4.6 · SRT and WebVTT support."), + ("GeoJournal Field Notes", "Create location-aware field notes with offline maps.", "journal maps offline field notes", "Version 2.5.7 · GPX import · coordinates optional."), + ("DiagramLite Flow Editor", "Small vector diagram editor with SVG export.", "diagram svg flowchart editor", "Version 0.8.9 · 24 bundled shapes · autosave enabled."), + ("Checksum Desk", "Generate and compare common file checksums locally.", "checksum sha256 files security", "Version 1.2.0 · SHA-256, SHA-512, and BLAKE2."), + ("CaptionCraft Transcriber", "Manual caption authoring workspace with keyboard controls.", "captions accessibility video editor", "Version 2.0.3 · WebVTT export · waveform preview."), + ("GardenPlot Planner", "Lay out beds and track crop rotations by season.", "garden planner crops open source", "Version 4.3.0 · metric and imperial grids."), + ("FontLedger Collection Viewer", "Preview locally installed font families and metadata.", "fonts typography viewer design", "Version 1.1.8 · specimen PDF export."), + ], + }, + "Images": { + "ext": "jpg", "mime": "image/jpeg", "license": "Creative Commons image", + "items": [ + ("London Skyline at Blue Hour", "A wide cityscape over the Thames as evening lights appear.", "london skyline city travel bridge", "Resolution 3840 × 2160 · captured at 20:14 · lens 24 mm."), + ("New York Skyline at Sunset", "Manhattan towers against a clear pastel sunset.", "new york skyline city sunset", "Resolution 3840 × 2160 · ISO 200 · exposure 1/80 s."), + ("Denali Reflection Panorama", "Snow-covered Denali reflected in still tundra water.", "denali mountain alaska panorama nature", "Resolution 2824 × 2176 · morning light · elevation viewpoint 640 m."), + ("Library Reading Room Windows", "Tall windows and long study tables in a historic reading room.", "library architecture reading room", "Resolution 2400 × 1600 · natural light · no people."), + ("Atlantic Coast Boardwalk", "Weathered boards leading through dunes toward the sea.", "ocean coast boardwalk landscape", "Resolution 3000 × 2000 · late afternoon · focal length 35 mm."), + ("Community Garden Pollinators", "Bees visiting purple flowers in a neighborhood garden.", "garden flowers bees nature", "Resolution 2200 × 1467 · macro crop · photographed in July."), + ("Grand Lake Sunrise", "Warm sunrise light over a mountain lake and surrounding ridges.", "mountain lake sunrise colorado", "Resolution 3200 × 2133 · tripod capture · 06:21 local time."), + ("Ceramic Studio Workbench", "Tools, clay, and unfinished vessels on a working studio table.", "ceramics art studio craft", "Resolution 2600 × 1733 · window light · documentary series."), + ("Red Bicycle by Brick Wall", "A city bicycle parked beside a warm red-brick facade.", "bicycle city street red", "Resolution 2400 × 1600 · 50 mm lens · overcast light."), + ("Winter Pines After Snow", "Fresh snow resting on dense evergreen branches.", "winter snow trees forest", "Resolution 3000 × 2000 · temperature -6°C · polarizing filter."), + ("Music Notebook and Fountain Pen", "A patterned music notebook and fountain pen arranged on a wooden desk.", "notebook fountain pen desk music", "Resolution 2400 × 1600 · overhead composition · daylight."), + ("Harbor Boats in Morning Fog", "Small sailboats emerging through pale harbor fog.", "harbor boats fog water", "Resolution 2800 × 1867 · 85 mm lens · photographed at 07:03."), + ("Wildflower Trail in Spring", "A narrow hillside trail lined with yellow and blue flowers.", "wildflowers trail spring hiking", "Resolution 3200 × 2133 · elevation 1,120 m · April capture."), + ("Classic Camera Detail", "Close view of the controls on a restored mechanical camera.", "camera vintage photography detail", "Resolution 2500 × 1667 · focus-stacked from six frames."), + ("Rainy City Street at Night", "Wet pavement and storefront lights on a quiet city street after dark.", "rain city street night lights", "Resolution 2400 × 1600 · available-light photograph · monochrome."), + ("Map and Compass Flat Lay", "A paper trail map, field compass, and pencil arranged for a hike.", "map compass hiking navigation", "Resolution 3000 × 2000 · overhead studio light · north arrow visible."), + ], + }, + "Books": { + "ext": "epub", "mime": "application/epub+zip", "license": "Public domain text", + "items": [ + ("The Secret Garden Illustrated Edition", "A carefully proofread edition of the classic garden story.", "classic fiction garden children", "338 pages · EPUB 3 · 12 original illustrations."), + ("A Study in Scarlet", "The first Sherlock Holmes novel in a clean reflowable edition.", "classic mystery sherlock holmes", "164 pages · chapter navigation · British spelling retained."), + ("The Time Machine", "H. G. Wells's compact science-fiction novel with editorial notes.", "science fiction classic time travel", "128 pages · 12 chapters · notes begin after page 116."), + ("Anne of Green Gables", "A reflowable edition with a Prince Edward Island map.", "classic fiction anne canada", "412 pages · 38 chapters · includes one regional map."), + ("The Adventures of Tom Sawyer", "A proofread edition with a historical-context introduction.", "classic fiction mark twain adventure", "296 pages · 35 chapters · introduction by Open Shelf editors."), + ("The Wonderful Wizard of Oz", "A color-illustrated EPUB edition of the original 1900 story.", "classic fantasy oz illustrated", "214 pages · 24 chapters · 20 color plates."), + ("Walden", "Thoreau's reflections on simple living with linked endnotes.", "essays nature philosophy thoreau", "384 pages · linked endnotes · 18 chapter essays."), + ("Pride and Prejudice", "A typographically polished edition with character index.", "classic romance austen fiction", "432 pages · 61 chapters · character index included."), + ("Frankenstein 1818 Text", "The original 1818 edition with a concise textual history.", "gothic science fiction shelley", "280 pages · 1818 text · three-volume structure retained."), + ("The Jungle Book", "Stories and poems in a navigable illustrated edition.", "classic stories kipling jungle", "246 pages · 14 illustrations · poems indexed separately."), + ("Meditations Public Domain Translation", "A clear English translation arranged by book and section.", "philosophy stoicism marcus aurelius", "192 pages · 12 books · searchable section numbers."), + ("The Federalist Papers", "All 85 essays with author and topic index.", "history politics essays constitution", "672 pages · 85 essays · searchable topic index."), + ("The Anti-Federalist Papers Selection", "A selected set of arguments opposing ratification, with editorial context.", "history politics essays constitution federalist papers", "244 pages · 24 selected essays · chronological reading list."), + ("Federalist Papers Study Questions", "Classroom prompts organized around major constitutional themes.", "history politics education federalist papers", "118 pages · 60 study questions · instructor notes appendix."), + ("Federalist Papers Author Concordance", "A reference concordance comparing commonly attributed authorship.", "history politics reference federalist papers", "206 pages · author tables · no full essay text."), + ("Federalist Papers Historical Reader", "Speeches, letters, and newspaper extracts from the ratification debate.", "history politics primary sources federalist papers", "356 pages · 41 source extracts · timeline included."), + ("Federalist Papers Constitutional Index", "A subject index linking constitutional clauses to related debates.", "history politics constitution federalist papers", "174 pages · clause index · cross-references only."), + ("Grimms Household Tales Selection", "Thirty selected tales in a reflowable reading edition.", "fairy tales folklore grimm", "318 pages · 30 tales · content notes included."), + ("The Souls of Black Folk", "Du Bois's landmark essays with preserved musical epigraphs.", "history essays sociology du bois", "286 pages · 14 essays · musical bars encoded as images."), + ("Leaves of Grass 1892 Edition", "The deathbed edition with section-level navigation.", "poetry whitman american", "476 pages · 17 sections · line breaks preserved."), + ("Twenty Thousand Leagues Under the Seas", "An illustrated translation of Verne's undersea adventure.", "adventure science fiction ocean verne", "512 pages · 47 chapters · 32 illustrations."), + ], + }, + "Documents": { + "ext": "pdf", "mime": "application/pdf", "license": "Open educational resource", + "items": [ + ("Urban Tree Inventory Field Guide", "A practical guide to measuring, identifying, and recording street trees.", "trees city field guide environment", "64 pages · revision 2.1 · diameter worksheet on page 41."), + ("Community Workshop Facilitation Notes", "Reusable agendas and exercises for small public workshops.", "community workshop facilitation guide", "38 pages · six agenda templates · accessibility checklist on page 32."), + ("Beginner Map Reading Workbook", "Exercises covering scale, symbols, contour lines, and coordinates.", "maps navigation workbook education", "72 pages · 24 exercises · answer key begins on page 66."), + ("Open Photography Metadata Handbook", "A reference to common EXIF, IPTC, and rights fields.", "photography metadata exif handbook", "54 pages · field matrix on page 17 · version 1.4."), + ("Home Energy Audit Checklist", "Room-by-room observations for a non-invasive home energy review.", "energy home checklist sustainability", "22 pages · 87 checklist items · climate notes appendix."), + ("Small Archive Digitization Plan", "A staged plan for naming, scanning, metadata, storage, and QA.", "archive scanning digitization workflow", "46 pages · 12-week sample schedule · risk register on page 39."), + ("Rain Garden Planting Guide", "Site selection and plant lists for compact residential rain gardens.", "garden rain water plants guide", "58 pages · three planting zones · maintenance calendar on page 49."), + ("Accessible Event Planning Workbook", "Prompts for venue, communication, sensory, and mobility access.", "accessibility events workbook planning", "44 pages · 63 prompts · vendor questions on page 29."), + ("Volunteer Trail Survey Form", "Printable forms for recording trail surface and drainage conditions.", "hiking trail survey form", "18 pages · four field forms · condition codes on page 5."), + ("Local History Interview Toolkit", "Consent, recording, description, and preservation guidance.", "history interview oral archive", "66 pages · sample release on page 55 · metadata sheet on page 59."), + ("Public Data Cleaning Recipes", "Spreadsheet-first techniques for dates, categories, and missing values.", "data spreadsheet cleaning tutorial", "82 pages · 19 recipes · validation checklist on page 78."), + ("Neighborhood Bird Count Protocol", "A repeatable ten-minute observation protocol for volunteers.", "birds citizen science protocol", "26 pages · ten-minute count · weather codes on page 12."), + ("Creative Commons Licensing Primer", "A plain-language guide to the six standard CC licenses.", "copyright creative commons licensing", "34 pages · license comparison chart on page 16."), + ("Remote Study Group Playbook", "Roles, meeting formats, and reflection prompts for peer study.", "study remote learning playbook", "40 pages · four meeting formats · facilitator cards appendix."), + ("Museum Label Writing Guide", "Techniques for concise, accessible object labels and panels.", "museum writing accessibility guide", "52 pages · 75-word label exercise on page 21."), + ("Community Garden Crop Calendar", "A temperate-climate planting and harvest planning calendar.", "garden crops calendar planning", "30 pages · zone 6 reference · succession table on page 24."), + ], + }, + "Archives": { + "ext": "zip", "mime": "application/zip", "license": "Creative Commons collection", + "items": [ + ("Urban Sketching Practice Sheets", "Printable perspective, texture, and value exercises.", "art drawing worksheets archive", "28 files · 44.6 MB unpacked · includes PDF and PNG formats."), + ("Open Map Symbol Collection", "A compact set of SVG symbols for community mapping.", "maps icons svg open data", "146 files · SVG format · symbol index included."), + ("Birdsong Identification Samples", "Short labeled clips for common woodland and garden birds.", "birds audio samples nature", "32 files · 96 MB unpacked · WAV and metadata CSV."), + ("Public Domain Botanical Plates", "Scanned botanical plates cleaned for classroom use.", "plants botanical images education", "48 files · 212 MB unpacked · 300 dpi JPEG."), + ("Accessible Presentation Templates", "High-contrast slide layouts with reading-order notes.", "accessibility slides templates", "14 files · PPTX and ODP · font list included."), + ("Community Survey Starter Pack", "Editable questionnaires, consent language, and coding sheets.", "survey community research templates", "21 files · DOCX, ODT, and XLSX formats."), + ("Field Recording Metadata Forms", "Sheets for location, equipment, rights, and technical notes.", "audio field recording metadata", "17 files · printable and spreadsheet versions."), + ("Historic Map Georeference Samples", "Practice maps with control points and completed examples.", "maps history gis tutorial", "26 files · GeoTIFF and CSV · five completed examples."), + ("Beginner Python Data Exercises", "Small CSV datasets and notebooks for introductory analysis.", "python data education notebooks", "39 files · 18 exercises · solutions in separate folder."), + ("Neighborhood Photo Walk Prompts", "Prompt cards and release forms for a group photo walk.", "photography community prompts", "24 files · 18 prompt cards · bilingual release form."), + ("Garden Planning Grid Pack", "Printable bed grids in metric and imperial dimensions.", "garden planning printable grids", "36 files · PDF and SVG · six page sizes."), + ("Oral History Audio Test Files", "Synthetic calibration clips for a digitization workflow.", "audio archive calibration testing", "12 files · WAV format · tones and spoken test counts."), + ("Open Icon Accessibility Set", "Simple interface icons with names and usage notes.", "icons accessibility interface svg", "180 files · SVG and PNG · 24 px and 48 px sizes."), + ("Classroom Weather Data Pack", "One year of fictional station readings for data lessons.", "weather data classroom csv", "13 files · 365 daily rows · data dictionary included."), + ("Local Newsletter Layout Kit", "Editable two- and four-page community newsletter layouts.", "newsletter design templates community", "18 files · Scribus and PDF · three color variants."), + ("Trail Sign Vector Collection", "Editable wayfinding and safety sign illustrations.", "trail hiking signs vector", "64 files · SVG and PDF · monochrome and color variants."), + ], + }, +} + +THUMBNAILS = [ + "images/london.jpg", + "images/new-york.jpg", + "images/denali.jpg", + "images/library-reading-room.jpg", + "images/atlantic-boardwalk.jpg", + "images/garden-pollinators.jpg", + "images/alpine-lake-mist.jpg", + "images/ceramic-workbench.jpg", + "images/red-bicycle-brick-wall.jpg", + "images/winter-pines-snow.jpg", + "images/notebook-fountain-pen.jpg", + "images/harbor-boats-fog.jpg", + "images/wildflower-trail.jpg", + "images/classic-camera.jpg", + "images/rainy-window-lights.jpg", + "images/map-compass.jpg", +] + + +def seed_database(): + """Seed the public catalog once; an existing catalog is a complete no-op.""" + if FileItem.query.filter_by(public=True).count() > 0: + return + file_id = 1 + for category, spec in CATALOG.items(): + for index, (title, description, tags, detail) in enumerate(spec["items"]): + filename = f"{title}.{spec['ext']}" + thumbnail = "" + if category == "Images": + thumbnail = THUMBNAILS[index] + item = FileItem( + id=file_id, + filename=filename, + slug=f"{slugify(filename)}-{file_id}", + category=category, + extension=spec["ext"], + mime_type=spec["mime"], + size_bytes=(index + 3) * (1_380_000 if category in {"Music", "Video"} else 438_000), + description=description, + tags=tags, + license_name=spec["license"], + uploader_name=UPLOADERS[(file_id + index) % len(UPLOADERS)], + public=True, + featured=index in {1, 6}, + thumbnail=thumbnail, + preview_text=detail, + uploaded_at=SEED_TIME - timedelta(days=(index * 5 + file_id % 9)), + modified_at=SEED_TIME - timedelta(days=(index * 3 + file_id % 7)), + download_count=670 + ((file_id * 733) % 48_000), + rating=round(3.8 + ((file_id * 7) % 13) / 10, 1), + ) + db.session.add(item) + file_id += 1 + db.session.commit() + + +BENCHMARK_USERS = [ + ("alice.j@test.com", "Alice Johnson", "Seattle, Washington", "Photographer and community archive volunteer."), + ("bob.c@test.com", "Bob Chen", "Austin, Texas", "Maps, field recordings, and open-data projects."), + ("carol.d@test.com", "Carol Davis", "Chicago, Illinois", "Teacher building accessible classroom resources."), + ("david.k@test.com", "David Kim", "Atlanta, Georgia", "Designer and neighborhood garden coordinator."), +] + +PRIVATE_FILES = [ + ("Quarterly retreat budget.xlsx", "Documents", 184_320, "Working budget with venue and travel estimates."), + ("Seattle photo selects.zip", "Archives", 8_340_000, "Shortlisted city images for the fall exhibit."), + ("Field recording notes.docx", "Documents", 94_208, "Location and microphone notes from the coast session."), + ("Reading list autumn.txt", "Documents", 12_288, "Personal reading list and library holds."), + ("Community map draft.pdf", "Documents", 2_430_000, "Draft map for the public workshop."), + ("Old outline.txt", "Documents", 7_168, "Superseded outline retained in Trash."), +] + + +def seed_benchmark_users(): + """Seed four benchmark accounts and their state once; reruns are no-ops.""" + if User.query.filter_by(email="alice.j@test.com").first(): + return + public_files = FileItem.query.filter_by(public=True).order_by(FileItem.id).all() + private_id = (public_files[-1].id if public_files else 0) + 1 + for user_index, (email, name, location, bio) in enumerate(BENCHMARK_USERS): + user = User( + id=user_index + 1, + email=email, + display_name=name, + location=location, + bio=bio, + plan="Premium" if user_index == 3 else "Free", + storage_limit_mb=102400 if user_index == 3 else 15360, + joined_at=SEED_TIME - timedelta(days=800 - user_index * 73), + ) + user.set_password(BENCHMARK_PASSWORD) + db.session.add(user) + db.session.flush() + folders = [] + for folder_index, folder_name in enumerate(("Work", "Photos", "Shared Projects", "Music")): + folder = Folder( + user_id=user.id, + name=folder_name, + created_at=SEED_TIME - timedelta(days=150 - folder_index * 9 - user_index), + ) + db.session.add(folder) + db.session.flush() + folders.append(folder) + owned = [] + for item_index, (base_name, category, size, description) in enumerate(PRIVATE_FILES): + prefix = ("Alice", "Bob", "Carol", "David")[user_index] + filename = f"{prefix} {base_name}" + extension = filename.rsplit(".", 1)[1].lower() + item = FileItem( + id=private_id, + owner_id=user.id, + folder_id=folders[item_index % len(folders)].id if item_index < 5 else None, + filename=filename, + slug=f"{slugify(filename)}-{private_id}", + category=category, + extension=extension, + mime_type="application/octet-stream", + size_bytes=size + user_index * 4096, + description=description, + tags="private personal benchmark", + license_name="Private", + uploader_name=name, + public=False, + deleted=item_index == 5, + preview_text=f"Private file owned by {name}. {description}", + uploaded_at=SEED_TIME - timedelta(days=40 - item_index * 3 + user_index), + modified_at=SEED_TIME - timedelta(days=10 - item_index + user_index), + download_count=0, + rating=0, + ) + db.session.add(item) + owned.append(item) + private_id += 1 + for offset in range(4): + public = public_files[(user_index * 19 + offset * 9) % len(public_files)] + db.session.add(Favorite(user_id=user.id, file_id=public.id, created_at=SEED_TIME - timedelta(days=12 + offset))) + for offset in range(3): + public = public_files[(user_index * 23 + offset * 11 + 5) % len(public_files)] + db.session.add(SavedFile(user_id=user.id, file_id=public.id, created_at=SEED_TIME - timedelta(days=20 + offset))) + for offset in range(2): + public = public_files[(user_index * 13 + offset * 17 + 2) % len(public_files)] + db.session.add(DownloadLog(user_id=user.id, file_id=public.id, downloaded_at=SEED_TIME - timedelta(days=offset + 2))) + db.session.add(SharedLink( + user_id=user.id, + file_id=owned[0].id, + token=f"demo-{user_index + 1}-retreat-budget", + permission="download" if user_index % 2 else "view", + label="Planning group", + created_at=SEED_TIME - timedelta(days=5 + user_index), + )) + if user_index == 3: + db.session.add(PlanOrder( + user_id=user.id, + plan_name="Premium", + billing_period="annual", + amount=77.88, + card_last4="4242", + status="Active", + created_at=SEED_TIME - timedelta(days=44), + )) + for index in range(12): + db.session.add(Comment( + user_id=(index % 4) + 1, + file_id=public_files[(index * 7 + 3) % len(public_files)].id, + body=( + "The detail notes and file metadata were especially useful for our workshop." + if index % 2 == 0 else + "Preview opened correctly, and the format information matched the download." + ), + created_at=SEED_TIME - timedelta(days=30 - index), + )) + db.session.commit() + + +if __name__ == "__main__": + with _app.app.app_context(): + db.create_all() + seed_database() + seed_benchmark_users() + print(f"seeded {FileItem.query.count()} files and {User.query.count()} users") diff --git a/sites/4shared/static/css/.gitkeep b/sites/4shared/static/css/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/4shared/static/css/main.css b/sites/4shared/static/css/main.css new file mode 100644 index 000000000..f0acbec3a --- /dev/null +++ b/sites/4shared/static/css/main.css @@ -0,0 +1,353 @@ +:root { + --blue: #0797f6; + --blue-dark: #087fce; + --sky: #7fc8f6; + --ink: #3d4652; + --muted: #8b9299; + --line: #e1e4e7; + --wash: #f4f5f6; + --footer: #30383f; + --shadow: 0 4px 14px rgba(38, 53, 66, .14); +} + +* { box-sizing: border-box; } +html { font-family: Arial, Helvetica, sans-serif; color: var(--ink); background: #fff; } +body { margin: 0; line-height: 1.42; font-size: 14px; } +a { color: var(--blue-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +button, input, select, textarea { font: inherit; } +.container { width: min(1170px, calc(100% - 40px)); margin-inline: auto; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } + +.topbar { height: 82px; background: #fff; display: flex; align-items: center; position: relative; z-index: 20; } +.topbar-inner { display: flex; align-items: center; gap: 28px; } +.home-topbar .topbar-inner { justify-content: center; } +.home-topbar .header-actions { position: absolute; right: max(20px, calc((100vw - 1170px) / 2)); } +.brand { display: inline-flex; align-items: baseline; color: #058ff0; font-size: 28px; font-weight: 700; letter-spacing: -1.7px; line-height: 1; } +.brand:hover { text-decoration: none; } +.brand-four { font-size: 36px; letter-spacing: -3px; } +.header-search { flex: 1; display: flex; max-width: 790px; border: 1px solid #d9dde0; border-radius: 28px; overflow: hidden; box-shadow: 0 3px 8px rgba(0,0,0,.10); } +.header-search input { flex: 1; min-width: 0; border: 0; outline: 0; padding: 14px 20px; color: #4d555d; } +.header-search button { border: 0; background: #fff; color: #555d64; font-size: 25px; padding: 4px 18px 8px; cursor: pointer; } +.header-actions { margin-left: auto; display: flex; align-items: center; gap: 22px; white-space: nowrap; } +.signin-pill { border: 2px solid #9ed6fb; color: #078df0; border-radius: 24px; padding: 8px 17px; font-weight: 700; } +.signin-pill:hover { text-decoration: none; background: #f3faff; } +.user-link { color: #474d52; font-weight: 700; } +.user-dot { color: #a9afb4; font-size: 24px; vertical-align: middle; margin-right: 7px; } +.bell-link { color: #48515a; font-size: 20px; } + +.flash { padding: 12px 16px; border-radius: 5px; margin: 10px 0; } +.flash.success { background: #e8f8f1; color: #146e49; } +.flash.error { background: #fff0f0; color: #a63d3d; } +.flash.message { background: #edf7ff; color: #24638e; } + +.hero { position: relative; overflow: hidden; text-align: center; } +.real-home { background: linear-gradient(#fff 3%, #edf9ff 82%, #e3f4fd); min-height: 790px; padding: 40px 0 70px; } +.hero-inner { position: relative; z-index: 2; } +.hero h1 { color: #050505; font-size: 34px; line-height: 1.15; margin: 0 0 14px; letter-spacing: -1px; } +.hero p { color: #8d9297; font-size: 19px; font-weight: 700; margin: 0 0 40px; } +.cloud { position: absolute; z-index: 1; width: 520px; height: 170px; border-radius: 100px; background: rgba(255,255,255,.88); filter: drop-shadow(0 -10px 0 rgba(255,255,255,.25)); bottom: 160px; } +.cloud::before, .cloud::after { content: ""; position: absolute; border-radius: 50%; background: inherit; } +.cloud::before { width: 230px; height: 230px; left: 60px; top: -95px; } +.cloud::after { width: 300px; height: 300px; right: 15px; top: -125px; } +.cloud-a { left: -150px; } +.cloud-b { right: -150px; transform: scale(.9); } +.hero-search { display: flex; width: min(750px, 100%); margin: 0 auto; border: 2px solid #a4d8f8; border-radius: 31px; background: white; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,.14); } +.hero-search input { flex: 1; min-width: 0; border: 0; outline: 0; padding: 17px 24px; font-size: 16px; } +.hero-search button { width: 58px; height: 54px; margin: 3px; border: 0; border-radius: 50%; background: var(--blue); color: #fff; font-size: 27px; cursor: pointer; } +.or-separator { margin: 15px 0; color: #333; font-weight: 700; } +.drop-zone { width: min(750px, 100%); min-height: 225px; margin: 0 auto; border: 2px dashed var(--blue); border-radius: 24px; display: flex; align-items: center; justify-content: center; flex-direction: column; color: var(--blue); background: rgba(237,249,255,.55); } +.drop-zone:hover { text-decoration: none; background: rgba(255,255,255,.75); } +.upload-artwork { width: 170px; height: 70px; object-fit: contain; margin-bottom: 10px; } +.upload-button { display: inline-flex; justify-content: center; min-width: 235px; padding: 13px 25px; margin-top: 18px; border-radius: 26px; background: var(--blue); color: #fff; font-weight: 700; } +.hero-categories { display: grid; grid-template-columns: repeat(6, 1fr); gap: 35px; margin: 75px auto 0; max-width: 1120px; } +.hero-categories a { height: 152px; border: 1px solid #d9dde0; border-radius: 22px; background: rgba(255,255,255,.94); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; color: #3f464c; box-shadow: 0 1px 3px rgba(0,0,0,.06); } +.hero-categories a:hover { text-decoration: none; border-color: #9ed6fb; } +.hero-categories b { font-size: 42px; line-height: 1; color: #ff902c; font-weight: 400; } +.hero-categories a:nth-child(2) b { color: #f43f57; } +.hero-categories a:nth-child(3) b { color: #54c735; } +.hero-categories a:nth-child(4) b { color: #775cf5; } +.hero-categories a:nth-child(5) b { color: #728b9d; } +.hero-categories a:nth-child(6) b { color: #aab1b7; } + +.app-band { text-align: center; padding: 65px 0 80px; background: #fff; } +.app-band h2 { margin: 0; font-size: 31px; color: #090909; } +.app-band p { color: #999fa5; font-size: 17px; font-weight: 700; } +.qr-motif { position: relative; width: 160px; height: 160px; margin: 20px auto 0; } +.qr-motif img { position: absolute; display: block; } +.qr-frame { inset: 0; width: 160px; height: 160px; } +.qr-code { inset: 16px; width: 128px; height: 128px; } +.app-badges { display: flex; justify-content: center; gap: 16px; margin-top: 22px; } +.store-badge { min-width: 215px; background: #22282d; color: white; border-radius: 28px; padding: 10px 22px; display: inline-flex; align-items: center; justify-content: center; gap: 10px; line-height: 1.1; text-align: left; } +.store-badge > img { width: 25px; height: 25px; object-fit: contain; } +.store-badge > span { display: flex; flex-direction: column; } +.store-badge small { font-size: 9px; } +.store-badge strong { font-size: 15px; } + +.search-page { padding: 0 0 70px; min-height: 65vh; } +.search-tabs { display: flex; justify-content: center; gap: 14px; flex-wrap: wrap; padding: 0 0 30px; } +.search-tabs a { border: 1px solid #d8dce0; border-radius: 22px; color: #454d55; padding: 9px 22px; font-weight: 700; } +.search-tabs a:hover { text-decoration: none; border-color: #a5abb0; } +.search-tabs a.active { color: #fff; background: #858585; border-color: #858585; } +.result-toolbar { display: flex; justify-content: space-between; align-items: center; color: #9b9fa3; margin: 0 14px 20px; } +.filters { display: flex; align-items: center; gap: 8px; } +.filters select { border: 0; background: #f4f5f6; color: #6f777e; padding: 7px 10px; border-radius: 15px; } +.filters button { border: 0; background: none; font-size: 21px; color: #78818a; cursor: pointer; } +.result-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 28px 30px; } +.result-row { min-height: 154px; border: 1px solid #e0e2e4; border-radius: 24px; overflow: hidden; display: grid; grid-template-columns: 158px 1fr 48px; background: #fff; position: relative; } +.result-visual { display: flex; align-items: center; justify-content: center; background: #7dc4f3; min-width: 0; overflow: hidden; } +.result-visual img { width: 100%; height: 100%; object-fit: cover; } +.media-glyph { color: #d8f0ff; font-size: 67px; line-height: 1; text-shadow: 0 8px 0 rgba(0,0,0,.04); } +.type-documents, .type-archives { background: #f6f5ef; } +.type-documents .media-glyph, .type-archives .media-glyph { color: #8fa3b1; font-size: 28px; text-shadow: none; } +.result-info { padding: 20px 10px 18px 20px; min-width: 0; } +.result-info h3 { margin: 0 0 3px; font-size: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.result-info h3 a { color: #414a53; } +.result-info p { margin: 0; color: #686f75; } +.result-owner { margin-top: 58px; color: #8a9096; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.result-add { display: grid; place-items: start center; padding-top: 12px; background: #f3f4f5; color: #46505a; border-radius: 0 0 0 28px; font-size: 28px; } +.result-add:hover { text-decoration: none; background: #ebedef; } + +.page, .file-page { padding: 25px 0 70px; min-height: 62vh; } +.page-title { margin: 0 0 5px; font-size: 28px; color: #222; } +.page-lede { color: var(--muted); margin: 0 0 25px; } +.section { padding: 45px 0; } +.section.alt { background: #f7f8f9; } +.section-head { display: flex; align-items: end; justify-content: space-between; margin-bottom: 20px; } +.section-head h2 { margin: 0; color: #272d32; } +.section-head p { margin: 5px 0 0; color: var(--muted); } +.file-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; } +.file-card { border: 1px solid #e0e3e5; border-radius: 20px; background: #fff; overflow: hidden; min-width: 0; position: relative; } +.file-thumb { height: 126px; display: flex; align-items: center; justify-content: center; background: #7dc4f3; overflow: hidden; } +.file-thumb img { width: 100%; height: 100%; object-fit: cover; } +.file-icon { display: grid; place-items: center; width: 58px; height: 70px; border: 2px solid rgba(255,255,255,.85); border-radius: 8px; color: #fff; text-transform: uppercase; font-size: 11px; } +.file-card-body { min-width: 0; padding: 14px; } +.file-title { display: block; color: #434b53; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.file-meta { display: flex; gap: 7px; color: #969da3; font-size: 11px; margin-top: 7px; } +.card-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 11px; } +.chip, .tag-line span { border: 1px dotted #aeb4b9; border-radius: 15px; padding: 3px 8px; color: #7b838a; font-size: 10px; } +.rating { color: #f3a91b; } +.icon-button { border: 0; background: none; color: #9aa6b4; font-size: 19px; cursor: pointer; } +.icon-button.active { color: #e14c6d; } + +.media-preview { min-height: 390px; border-radius: 24px; background: #238ebb; display: flex; align-items: center; justify-content: center; overflow: hidden; } +.media-preview img { width: 100%; max-height: 610px; object-fit: contain; background: #edf1f4; } +.media-preview > .file-icon { transform: scale(1.6); } +.waveform { width: 94%; height: 260px; display: flex; align-items: center; justify-content: center; gap: 8px; position: relative; } +.waveform i { width: 5px; height: 80px; background: #046da0; opacity: .9; } +.waveform i:nth-child(3n) { height: 150px; } +.waveform i:nth-child(4n) { height: 115px; } +.waveform i:nth-child(5n) { height: 185px; } +.waveform span { position: absolute; bottom: 8px; color: #fff; font-size: 46px; } +.file-heading { display: flex; justify-content: space-between; gap: 20px; align-items: center; padding: 26px 0 12px; } +.file-heading small { color: #41484e; font-weight: 700; } +.detail-title { color: #313940; font-size: 27px; margin: 1px 0 6px; } +.detail-meta { color: #8c9297; margin: 0; } +.tag-line { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 10px; } +.heart-button { width: 54px; height: 54px; border: 0; border-radius: 50%; background: var(--blue); color: #fff; font-size: 31px; cursor: pointer; } +.detail-actions { display: flex; align-items: center; gap: 10px; padding: 18px 0 28px; border-bottom: 1px solid #e2e4e6; } +.detail-actions form { margin: 0; } +.primary-button, .secondary-button, .danger-button { display: inline-flex; align-items: center; justify-content: center; border-radius: 23px; padding: 10px 18px; font-weight: 700; cursor: pointer; } +.primary-button { background: var(--blue); color: #fff; border: 1px solid var(--blue); } +.primary-button:hover { background: var(--blue-dark); text-decoration: none; } +.secondary-button { background: #fff; color: #4a535b; border: 1px solid #d6dadd; } +.secondary-button:hover { border-color: var(--blue); color: var(--blue); text-decoration: none; } +.danger-button { color: #bd3f3f; background: #fff; border: 1px solid #e4bbbb; } +.wide { width: 100%; } +.detail-copy { padding: 5px 0 20px; } +.detail-copy h2, .comments h2 { font-size: 18px; color: #313940; } +.preview-note { background: #f5f7f8; padding: 14px 18px; border-radius: 10px; color: #56616a; } +.comments { padding: 0 0 28px; border-bottom: 1px solid #e1e3e5; } +.comments h2 small { color: #9da3a8; } +.comment-form { display: flex; gap: 12px; align-items: center; } +.comment-form textarea { flex: 1; min-height: 44px; height: 44px; border: 0; border-radius: 24px; background: #f0f1f2; padding: 12px 18px; resize: vertical; } +.comment-avatar { color: #aab0b5; font-size: 28px; } +.comment { padding: 14px 0; border-top: 1px solid #eee; } +.comment strong { display: block; } +.comment small { color: var(--muted); } +.related-head { margin-top: 42px; } +.related-grid { grid-template-columns: repeat(2, 1fr); } +.related-grid .file-card { display: grid; grid-template-columns: 155px 1fr; min-height: 145px; } +.related-grid .file-thumb { height: 100%; } + +.panel { border: 1px solid #e0e3e5; border-radius: 18px; background: #fff; padding: 24px; } +.auth-shell { max-width: 500px; margin: 45px auto; } +.auth-shell .panel { border: 0; box-shadow: 0 12px 35px rgba(0,0,0,.20); border-radius: 22px; } +.auth-shell h1 { text-align: center; color: #222; } +.center { text-align: center; } +.form-group { margin-bottom: 16px; } +.form-group label { display: block; margin: 0 0 6px; font-weight: 700; color: #41484f; } +.form-group input, .form-group select, .form-group textarea { width: 100%; border: 1px solid #ccd1d5; border-radius: 4px; padding: 10px 11px; background: #fff; } +.form-group textarea { min-height: 100px; resize: vertical; } +.form-hint, .auth-foot { color: var(--muted); font-size: 12px; } +.auth-foot { text-align: center; margin-top: 18px; } + +.account-layout { display: grid; grid-template-columns: 250px 1fr; gap: 24px; } +.account-nav { padding: 8px 0; min-height: 590px; border-right: 1px solid #e4e6e8; } +.account-nav a { display: block; color: #555e65; padding: 12px 20px; margin: 3px 0; border-radius: 24px 0 0 24px; font-weight: 700; } +.account-nav a:hover, .account-nav a.current { background: #e1f2fd; color: var(--blue-dark); text-decoration: none; } +.account-nav .account-add { display: inline-block; margin: 0 0 14px; padding: 10px 23px; border-radius: 24px; background: var(--blue); color: #fff; box-shadow: 0 4px 10px rgba(7,151,246,.28); } +.account-nav .account-add:hover, .account-nav .account-add.current { background: var(--blue-dark); color: #fff; } +.account-nav-separator { display: block; height: 1px; margin: 14px 20px; background: #e5e7e9; } +.account-card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } +.stat-card { border: 1px solid #e0e3e5; border-radius: 18px; padding: 20px; } +.stat-card strong { display: block; font-size: 24px; } +.stat-card span { color: var(--muted); } +.storage-bar { height: 7px; border-radius: 5px; background: #e6e9eb; overflow: hidden; } +.storage-bar i { display: block; height: 100%; background: var(--blue); } +.file-manager-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 20px; } +.inline-form { display: flex; gap: 8px; } +.inline-form input, .inline-form select { border: 1px solid #ccd2d7; border-radius: 20px; padding: 9px 13px; } +.folder-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 22px; } +.folder-card { padding: 16px; border-radius: 18px; background: #e2f2fc; color: #188bd5; } +.folder-card:hover { text-decoration: none; background: #d5edfc; } +.manager-table { width: 100%; border-collapse: collapse; } +.manager-table th { text-align: left; color: #8b9298; padding: 11px; border-bottom: 1px solid #e4e6e8; } +.manager-table td { padding: 13px 11px; border-bottom: 1px solid #e9ebed; vertical-align: middle; } +.manager-actions { display: flex; gap: 6px; flex-wrap: wrap; } +.manager-actions form { display: inline-flex; gap: 4px; } +.manager-actions button, .manager-actions a { border: 1px solid #d5dade; background: #fff; border-radius: 16px; color: #59636b; padding: 5px 9px; font-size: 11px; } +.manager-actions input, .manager-actions select { border: 1px solid #d5dade; border-radius: 14px; padding: 4px 7px; } +.breadcrumb { color: #7e878e; margin-bottom: 17px; font-size: 18px; } +.empty { padding: 46px; text-align: center; color: #92999f; border: 1px dashed #d4d8db; border-radius: 14px; } +.notice { background: #fff8df; border: 1px solid #efdda3; padding: 13px; border-radius: 7px; color: #776320; } + +.plan-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 420px)); justify-content: center; gap: 22px; } +.plan { border: 1px solid #dfe3e6; border-radius: 18px; padding: 28px; position: relative; background: #fff; } +.plan.recommended { border: 2px solid var(--blue); box-shadow: var(--shadow); } +.plan-badge { position: absolute; top: -11px; right: 20px; color: #fff; background: #ff8b31; border-radius: 13px; padding: 3px 10px; font-size: 10px; } +.plan-price { margin: 14px 0; font-size: 34px; } +.plan-price small { font-size: 12px; color: var(--muted); } +.plan ul { min-height: 150px; padding-left: 20px; color: #59636c; } +.premium-hero { padding: 55px 0 75px; text-align: center; background: radial-gradient(circle at 18% 14%, #eef9ff 0 10%, transparent 28%), radial-gradient(circle at 82% 12%, #eef9ff 0 10%, transparent 28%), #fff; } +.premium-hero > .container > h1 { color: #111; font-size: 32px; margin: 0 0 30px; } +.billing-toggle { display: inline-block; margin-bottom: 32px; } +.billing-toggle span { display: block; color: #ff8b31; font-weight: 700; font-size: 11px; } +.billing-toggle p { margin: 7px 0 0; color: #7d858c; } +.billing-toggle b { color: var(--blue); font-size: 24px; } +.four-plans { grid-template-columns: repeat(4, 1fr); max-width: 1100px; margin: 0 auto; gap: 14px; text-align: left; } +.four-plans .plan { padding: 24px; border-radius: 17px; min-width: 0; } +.four-plans .plan h2 { color: var(--blue); font-size: 17px; } +.four-plans .plan h4 { font-size: 10px; margin-top: 28px; } +.four-plans .plan ul { min-height: 160px; font-size: 12px; line-height: 1.8; } +.plan-current { display: block; background: #f0f1f2; color: #a0a6ab; border-radius: 22px; padding: 10px; text-align: center; font-weight: 700; } +.secure-copy { color: #9aa0a5; font-size: 11px; } +.feature-compare { max-width: 900px; margin: 80px auto 0; } +.feature-compare h2, .premium-faq h2 { color: #111; font-size: 26px; } +.compare-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr; border: 1px solid #e4e7e9; border-radius: 10px; overflow: hidden; text-align: left; } +.compare-grid > * { padding: 15px 20px; border-bottom: 1px solid #e7e9eb; } +.compare-grid > *:nth-child(3n) { background: #edf8ff; text-align: center; } +.compare-grid > *:nth-child(3n + 2) { text-align: center; } +.premium-faq { max-width: 650px; margin: 70px auto 0; text-align: left; } +.premium-faq h2 { text-align: center; } +.premium-faq details { border-bottom: 1px solid #e6e8ea; padding: 16px; } +.premium-faq summary { font-weight: 700; cursor: pointer; } + +.content-page { max-width: 1020px; } +.eyebrow, .story-tag { color: var(--blue-dark); font-size: 11px; font-weight: 800; letter-spacing: .12em; } +.story-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-top: 30px; } +.story-grid .panel h2 { font-size: 20px; line-height: 1.3; } +.story-grid time, .story-grid .story-tag { color: var(--muted); font-size: 11px; } +.converter-result { display: flex; justify-content: space-between; gap: 16px; margin: 20px 0; padding: 14px 16px; border: 1px solid #9bd5b9; border-radius: 8px; background: #ebfaf2; color: #176342; } +.converter-result span { font-size: 12px; } + +.site-footer { background: var(--footer); color: #fff; padding: 48px 0 28px; } +.footer-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 52px; } +.footer-grid h4 { margin: 0 0 13px; font-size: 13px; } +.footer-grid a { display: block; color: #959da3; margin: 8px 0; font-size: 12px; } +.footer-bottom { display: flex; align-items: end; justify-content: space-between; gap: 25px; margin-top: 75px; } +.footer-brand { color: #fff; } +.footer-brand .brand-four { color: var(--blue); } +.footer-pills { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; } +.footer-pills span { border: 1px solid #4e5860; border-radius: 22px; padding: 8px 15px; font-size: 11px; display: inline-flex; align-items: center; gap: 6px; } +.footer-pills img { width: 14px; height: 14px; object-fit: contain; } + +@media (max-width: 900px) { + .header-search { max-width: none; } + .user-link { max-width: 150px; overflow: hidden; text-overflow: ellipsis; } + .hero-categories { grid-template-columns: repeat(3, 1fr); } + .result-list { grid-template-columns: 1fr; } + .file-grid { grid-template-columns: repeat(2, 1fr); } + .account-layout { grid-template-columns: 190px 1fr; } + .account-card-grid { grid-template-columns: 1fr; } + .story-grid { grid-template-columns: 1fr; } + .folder-grid { grid-template-columns: repeat(2, 1fr); } + .footer-grid { grid-template-columns: repeat(3, 1fr); } +} + +@media (max-width: 600px) { + .container { width: min(100% - 24px, 1170px); } + .topbar { height: 62px; } + .brand { font-size: 21px; } + .brand-four { font-size: 28px; } + .home-topbar .header-actions { right: 12px; } + .signin-pill { padding: 5px 10px; font-size: 11px; } + .header-search { order: 3; flex-basis: 100%; box-shadow: none; } + .topbar:not(.home-topbar) { height: auto; padding: 10px 0; } + .topbar:not(.home-topbar) .topbar-inner { flex-wrap: wrap; gap: 8px; } + .header-search input { padding: 10px 14px; } + .header-search button { font-size: 20px; padding: 2px 13px 5px; } + .user-link { max-width: 115px; font-size: 11px; } + .bell-link { display: none; } + .real-home { min-height: 680px; padding-top: 30px; } + .hero h1 { font-size: 28px; } + .hero p { font-size: 15px; margin-bottom: 25px; } + .hero-search input { padding: 12px 16px; } + .hero-search button { width: 44px; height: 42px; font-size: 20px; } + .drop-zone { min-height: 165px; border-radius: 17px; } + .upload-artwork { width: 130px; height: 54px; } + .upload-button { min-width: 170px; padding: 10px 18px; } + .hero-categories { gap: 10px; margin-top: 42px; } + .hero-categories a { height: 92px; border-radius: 14px; gap: 8px; font-size: 11px; } + .hero-categories b { font-size: 26px; } + .cloud { display: none; } + .app-band { padding: 45px 0; } + .app-band h2 { font-size: 25px; } + .app-band p { font-size: 13px; } + .qr-motif { width: 136px; height: 136px; } + .qr-frame { width: 136px; height: 136px; } + .qr-code { inset: 14px; width: 108px; height: 108px; } + .app-badges { flex-direction: column; align-items: center; } + .store-badge { min-width: 205px; } + .search-tabs { gap: 7px; padding-bottom: 18px; } + .search-tabs a { padding: 6px 11px; font-size: 11px; } + .result-toolbar { align-items: flex-start; gap: 10px; } + .filters select { max-width: 104px; } + .result-row { min-height: 112px; grid-template-columns: 105px 1fr 36px; border-radius: 16px; } + .result-info { padding: 13px 7px 10px 12px; } + .result-info h3 { display: -webkit-box; font-size: 14px; line-height: 1.25; white-space: normal; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } + .result-owner { margin-top: 12px; font-size: 10px; } + .result-add { font-size: 21px; } + .media-glyph { font-size: 45px; } + .media-preview { min-height: 215px; border-radius: 15px; } + .waveform { height: 165px; gap: 3px; } + .waveform i { width: 3px; } + .waveform span { font-size: 30px; } + .file-heading { align-items: flex-start; } + .detail-title { font-size: 22px; } + .heart-button { width: 44px; height: 44px; } + .detail-actions { flex-wrap: wrap; } + .detail-actions .primary-button, .detail-actions .secondary-button { padding: 8px 12px; font-size: 11px; } + .comment-form { flex-wrap: wrap; } + .comment-form textarea { flex-basis: calc(100% - 50px); } + .file-grid, .related-grid { grid-template-columns: 1fr; } + .related-grid .file-card { grid-template-columns: 110px minmax(0, 1fr); } + .related-grid .file-title { display: -webkit-box; line-height: 1.3; white-space: normal; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 3; } + .account-layout { grid-template-columns: 1fr; } + .account-nav { display: flex; flex-wrap: wrap; overflow-x: visible; min-height: 0; border: 0; gap: 5px; padding: 0 0 12px; } + .account-nav a { flex: 0 0 auto; border-radius: 18px; background: #f3f4f5; padding: 8px 12px; font-size: 11px; } + .account-nav-separator { display: none; } + .panel { padding: 16px; border-radius: 14px; } + .file-manager-toolbar { display: block; } + .inline-form { margin-top: 10px; } + .folder-grid { grid-template-columns: 1fr 1fr; } + .manager-table thead { display: none; } + .manager-table tr { display: block; border: 1px solid #e1e4e6; border-radius: 12px; padding: 8px; margin-bottom: 10px; } + .manager-table td { display: block; border: 0; padding: 5px; } + .manager-actions { display: grid; } + .plan-grid { grid-template-columns: 1fr; } + .four-plans { grid-template-columns: 1fr; } + .footer-grid { grid-template-columns: repeat(2, 1fr); gap: 25px; } + .footer-bottom { display: block; margin-top: 45px; } + .footer-pills { justify-content: flex-start; margin-top: 25px; } +} diff --git a/sites/4shared/static/icons/.gitkeep b/sites/4shared/static/icons/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/4shared/static/icons/mark.svg b/sites/4shared/static/icons/mark.svg new file mode 100644 index 000000000..cf18fbd70 --- /dev/null +++ b/sites/4shared/static/icons/mark.svg @@ -0,0 +1 @@ + diff --git a/sites/4shared/static/js/.gitkeep b/sites/4shared/static/js/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/4shared/static/js/main.js b/sites/4shared/static/js/main.js new file mode 100644 index 000000000..46895be64 --- /dev/null +++ b/sites/4shared/static/js/main.js @@ -0,0 +1 @@ +document.addEventListener("DOMContentLoaded",()=>{const button=document.querySelector(".menu-button");const nav=document.querySelector(".category-nav");if(button&&nav){button.addEventListener("click",()=>{nav.hidden=!nav.hidden;button.setAttribute("aria-expanded",String(!nav.hidden));});}document.querySelectorAll("[data-confirm]").forEach(form=>form.addEventListener("submit",event=>{if(!window.confirm(form.dataset.confirm)){event.preventDefault();}}));}); diff --git a/sites/4shared/tasks.jsonl b/sites/4shared/tasks.jsonl new file mode 100644 index 000000000..218b0dd32 --- /dev/null +++ b/sites/4shared/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"4shared","id":"4shared--0","ques":"Search the Music catalog for nature ambience. Find the track whose detail page says it lasts 10:45, comes from a 48 kHz WAV source, and is normalized to -16 LUFS. Report the exact filename and uploader.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--1","ques":"Browse Images and find the city skyline photo whose detail page lists ISO 200 and an exposure of 1/80 second. Report its exact filename, uploader, and resolution.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--2","ques":"Search Books for classic fiction and inspect the candidates. Which exact filename has 61 chapters and includes a character index? Also report its displayed file size.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--3","ques":"Search broadly for garden planning resources. Inspect the plausible results until you find the PDF whose detail page says it has three planting zones and a maintenance calendar on page 49. Report the filename, page count, and uploader.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--4","ques":"Search broadly for accessibility and design resources. Inspect the plausible results until you find the app package whose detail page mentions a WCAG contrast preview. Report the exact filename, version, and license shown on the page.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--5","ques":"In Videos, compare Open Data Mapping Basics with City Cycling Route Planning by opening both detail pages. Which has the longer runtime, and what are both runtimes?","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--6","ques":"Find The Federalist Papers in Books, verify from its detail page that it contains all 85 essays and a searchable topic index, then use the Download button.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--7","ques":"Log in with alice.j@test.com and password TestPass123!. Find ArchivePeek File Inspector, add it to Favorites, then open Favorites to confirm it is present.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--8","ques":"Log in with alice.j@test.com and password TestPass123!. Find Rain Garden Planting Guide and save it to My 4shared, then open Saved files to confirm it is present.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--9","ques":"Log in with alice.j@test.com and password TestPass123!. Update the account location to Portland, Oregon and the bio to ‘Community archive volunteer and urban sketcher.’","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--10","ques":"Log in with bob.c@test.com and password TestPass123!. In My files, create a new root-level folder named Survey Exports.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--11","ques":"Log in with carol.d@test.com and password TestPass123!. Upload a private file record named accessibility-session-notes.pdf, size 640 KB, with description ‘Notes and action items from the accessibility session.’ into the Work folder.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--12","ques":"Log in with alice.j@test.com and password TestPass123!. Rename Alice Quarterly retreat budget.xlsx to 2027 Retreat Budget.xlsx and move it into the Shared Projects folder.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--13","ques":"Log in with david.k@test.com and password TestPass123!. Open Trash and restore David Old outline.txt.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--14","ques":"Log in with alice.j@test.com and password TestPass123!. Open Alice Field recording notes.docx and create a share link labeled Audio volunteers with Preview and download permission.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--15","ques":"Log in with bob.c@test.com and password TestPass123!. Find Beginner Map Reading Workbook and post the comment ‘The coordinate exercises are ideal for our Saturday workshop.’","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--16","ques":"Log in with bob.c@test.com and password TestPass123!. Upgrade to the annual Premium plan using cardholder Bob Chen and demo card 4242 4242 4242 4242. Confirm the resulting plan and storage allowance from My 4shared.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--17","ques":"Log in with carol.d@test.com and password TestPass123!. Create a root folder named Workshop Handouts, upload a private 384 KB file named spring-workshop-outline.pdf into it with description ‘Draft outline for the spring neighborhood workshop.’, rename it final-spring-workshop-outline.pdf, then create a preview-only share link labeled Planning committee.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--18","ques":"Compare Pride and Prejudice, Anne of Green Gables, and Twenty Thousand Leagues Under the Seas by opening each book’s detail page. Report which has the most pages, its exact page count, and how many chapters it contains; then log in with david.k@test.com and password TestPass123! and save that book to My 4shared.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} +{"web_name":"4shared","id":"4shared--19","ques":"Log in with alice.j@test.com and password TestPass123!. Search broadly for archive metadata resources and inspect the plausible results until you find the document with a 12-week sample schedule and a risk register on page 39. Add it to Favorites, download it, and report its exact filename and total page count.","web":"http://localhost:40023/","upstream_url":"https://www.4shared.com/"} diff --git a/sites/4shared/templates/.gitkeep b/sites/4shared/templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/4shared/templates/404.html b/sites/4shared/templates/404.html new file mode 100644 index 000000000..db35406fb --- /dev/null +++ b/sites/4shared/templates/404.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}File not found — 4shared{% endblock %}{% block content %}

We couldn't find that file

It may have moved, become private, or returned to its owner’s Trash.

Search all files
{% endblock %} diff --git a/sites/4shared/templates/_account_nav.html b/sites/4shared/templates/_account_nav.html new file mode 100644 index 000000000..f3c9b9fa6 --- /dev/null +++ b/sites/4shared/templates/_account_nav.html @@ -0,0 +1,12 @@ + diff --git a/sites/4shared/templates/_file_card.html b/sites/4shared/templates/_file_card.html new file mode 100644 index 000000000..4bf4d5355 --- /dev/null +++ b/sites/4shared/templates/_file_card.html @@ -0,0 +1,10 @@ + diff --git a/sites/4shared/templates/about.html b/sites/4shared/templates/about.html new file mode 100644 index 000000000..70aa86039 --- /dev/null +++ b/sites/4shared/templates/about.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}About — 4shared{% endblock %}{% block content %}

About 4shared

A search-first file storage and sharing experience.

Search, store and share

4shared brings public-file discovery together with personal cloud storage. People can browse by file category, preview metadata, keep favorites, organize their own folders, and create share links.

About this mirror

This WebHarbor contribution is a deterministic, offline recreation for browser-agent evaluation. Its database and state reset to an identical seed, and its public catalog is limited to benign educational, public-domain, and open-source metadata.

{% endblock %} diff --git a/sites/4shared/templates/account.html b/sites/4shared/templates/account.html new file mode 100644 index 000000000..256266ff9 --- /dev/null +++ b/sites/4shared/templates/account.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}My 4shared{% endblock %}{% block content %}

Welcome back, {{ current_user.display_name.split()[0] }}

Manage your cloud files and recent activity.

{% endblock %} diff --git a/sites/4shared/templates/account_edit.html b/sites/4shared/templates/account_edit.html new file mode 100644 index 000000000..95eb8e952 --- /dev/null +++ b/sites/4shared/templates/account_edit.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Account settings — 4shared{% endblock %}{% block content %}

Account settings

Keep your public profile information up to date.

{% endblock %} diff --git a/sites/4shared/templates/activity.html b/sites/4shared/templates/activity.html new file mode 100644 index 000000000..6a0c33e4f --- /dev/null +++ b/sites/4shared/templates/activity.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Activity — 4shared{% endblock %}{% block content %}

Activity

Your recent downloads and active share links.

{% endblock %} diff --git a/sites/4shared/templates/base.html b/sites/4shared/templates/base.html new file mode 100644 index 000000000..8301a58ff --- /dev/null +++ b/sites/4shared/templates/base.html @@ -0,0 +1,53 @@ + + + + + + {% block title %}4shared — free file sharing and storage{% endblock %} + + + + +
+
+ 4shared + {% if request.endpoint != 'index' or current_user.is_authenticated %} + + {% endif %} + +
+
+
+
+ {% for category, message in get_flashed_messages(with_categories=true) %}
{{ message }}
{% endfor %} +
+ {% block content %}{% endblock %} +
+ + + + diff --git a/sites/4shared/templates/blog.html b/sites/4shared/templates/blog.html new file mode 100644 index 000000000..f23eb1866 --- /dev/null +++ b/sites/4shared/templates/blog.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block title %}4shared Blog{% endblock %} +{% block content %} +
+

4SHARED BLOG

+

Ideas for organizing and sharing files

+
+

Build a folder structure that stays useful

Start with a few durable project folders, then use clear filenames and dates where they add context.

Read the organizing guide
+

Choose preview or download permissions

Use preview access when collaborators need to inspect a file, and download access when they need a working copy.

Learn about sharing
+

Find files with focused keywords

Combine topic, format, and distinctive metadata to narrow a large public catalog.

Try public search
+
+
+{% endblock %} diff --git a/sites/4shared/templates/category.html b/sites/4shared/templates/category.html new file mode 100644 index 000000000..1d2e11b0a --- /dev/null +++ b/sites/4shared/templates/category.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}{{ category }} files — 4shared{% endblock %}{% block content %}

{{ category }}

Browse safe public {{ category|lower }} files, ordered by community activity.

{% for file in files %}{% include '_file_card.html' %}{% endfor %}
{% endblock %} diff --git a/sites/4shared/templates/converter.html b/sites/4shared/templates/converter.html new file mode 100644 index 000000000..8ff0dd8c0 --- /dev/null +++ b/sites/4shared/templates/converter.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% block title %}{{ source_format|upper }} to PDF converter — 4shared{% endblock %} +{% block content %} +
+

FILE CONVERTER

+

{{ source_format|upper }} to PDF

+

Create a simulated PDF result without uploading file bytes or contacting an external service.

+ {% if converted_name %}
{{ converted_name }}Conversion ready
{% endif %} +
+
+ +
+
+{% endblock %} diff --git a/sites/4shared/templates/download_ready.html b/sites/4shared/templates/download_ready.html new file mode 100644 index 000000000..13774125a --- /dev/null +++ b/sites/4shared/templates/download_ready.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Download ready — 4shared{% endblock %}{% block content %}

Your download is ready

{{ file.filename }}

This offline mirror records the download but does not transfer executable or user-hosted content.

Return to file
{% endblock %} diff --git a/sites/4shared/templates/favorites.html b/sites/4shared/templates/favorites.html new file mode 100644 index 000000000..7e028336c --- /dev/null +++ b/sites/4shared/templates/favorites.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% set heading=title|default('Favorites') %}{% block title %}{{ heading }} — 4shared{% endblock %}{% block content %}

{{ heading }}

Files you kept for quick access.

{% endblock %} diff --git a/sites/4shared/templates/file_detail.html b/sites/4shared/templates/file_detail.html new file mode 100644 index 000000000..db97e7800 --- /dev/null +++ b/sites/4shared/templates/file_detail.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block title %}{{ file.filename }} — 4shared{% endblock %} +{% block content %} +
+
+ {% if file.thumbnail %}{{ file.stem }}{% elif file.category in ['Music','Video'] %}
{% else %}{{ file.extension }}{% endif %} +
+
+
{{ file.uploader_name }}

{{ file.filename }}

● {{ file.uploader_name }}  in  ▰ My 4shared   {{ file.uploaded_at.strftime('%b %d, %Y') }}

{{ file.extension|upper }}{{ file.size_bytes|filesize }}{{ file.category }}{{ file.license_name }}
+ {% if current_user.is_authenticated %}
{% endif %} +
+
+ ↗  Open in… + {% if current_user.is_authenticated %}
{% else %}+  To library{% endif %} +
+ {% if current_user.is_authenticated %}⌯  Share{% else %}⌯  Share{% endif %} +
+

About this file

{{ file.description }}

{{ file.preview_text }}
+

Comments {{ file.comments|length }}

{% if current_user.is_authenticated %}
{% else %}

Log in to comment.

{% endif %}{% for comment in file.comments %}
{{ comment.user.display_name }}{{ comment.created_at.strftime('%b %d, %Y') }}

{{ comment.body }}

{% endfor %}
+ + +
+{% endblock %} diff --git a/sites/4shared/templates/help.html b/sites/4shared/templates/help.html new file mode 100644 index 000000000..3722c4cf5 --- /dev/null +++ b/sites/4shared/templates/help.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Help and FAQ — 4shared{% endblock %}{% block content %}

How can we help?

Quick answers for searching, uploading, organizing, and sharing.

Search public files

Use one or several keywords, a topic, file extension, or category. Shorter queries often produce broader results.

Upload and organize

Sign in, choose Upload, add file details, and select a destination folder. You can rename or move it later.

Share a file

Open a file, choose Share file, then create a preview-only or download-enabled link.

Restore from Trash

Deleted files remain in Trash. Open Trash from My 4shared and choose Restore beside the file.

Preview formats

Images display directly. Music, video, books, documents, apps, and archives show descriptive preview metadata.

Free storage

Free accounts include 15 GB. Premium accounts in this mirror include 100 GB and faster-access features.

Convert documents

The source site links to PDF conversion tools for DOC, DOCX, PPT, PPTX, XLS, and XLSX formats. Conversion is informational in this offline mirror.

Safe benchmark content

All catalog records are benign educational or open-cultural metadata. No executable or user-hosted bytes are served.

{% endblock %} diff --git a/sites/4shared/templates/index.html b/sites/4shared/templates/index.html new file mode 100644 index 000000000..2a425c5b7 --- /dev/null +++ b/sites/4shared/templates/index.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+

Search, store and share easily

+

All your files: music, videos, apps and more

+ +
or
+ + + Drop files here or + Upload files + +
+ {% set icons = {'Music':'♫','Video':'▷','Apps':'♙','Images':'▧','Books':'▥'} %} + {% for item in ['Music','Video','Apps','Images','Books'] %}{{ icons[item] }}{{ item }}{% endfor %} + All Files +
+
+
+

Get 4shared App

Offline access  •  Faster downloads  •  Real-time updates

QR code to download the 4shared app
GET IT ONGoogle PlayDownload on theApp StoreExplore it onAppGallery
+{% endblock %} diff --git a/sites/4shared/templates/login.html b/sites/4shared/templates/login.html new file mode 100644 index 000000000..c5d524790 --- /dev/null +++ b/sites/4shared/templates/login.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Log in — 4shared{% endblock %}{% block content %}

Log in to 4shared

Access your files, folders, favorites, and share links.

New to 4shared? Sign up for free

{% endblock %} diff --git a/sites/4shared/templates/my_files.html b/sites/4shared/templates/my_files.html new file mode 100644 index 000000000..33c9d79c1 --- /dev/null +++ b/sites/4shared/templates/my_files.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}My files — 4shared{% endblock %}{% block content %}

My files

Organize your uploads into folders and create share links.

{% endblock %} diff --git a/sites/4shared/templates/premium.html b/sites/4shared/templates/premium.html new file mode 100644 index 000000000..0d1fd62bd --- /dev/null +++ b/sites/4shared/templates/premium.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% block title %}4shared Premium - Choose Your Plan{% endblock %} +{% block content %} +
+

Expand your storage with 4shared Premium

+
GET 15% OFF WITH YEARLY

Monthly     Yearly

+
+

Free 15 GB

$0
Current plan

INCLUDES

  • 15 GB of free storage
  • Priority download
  • No credit card required
+

Premium 100 GB

$6.49 / month, billed yearly
Choose annual

EVERYTHING IN FREE +

  • 100 GB of storage
  • Ads-free browsing
  • Direct downloads
  • File and account statistics
+ +

Premium 1 TB

$39.99 / year
Select 1 TB

EVERYTHING IN 500 GB +

  • 1 TB of storage
  • File backup and restore
  • Premium support
+
+

Cancel anytime. Secure payment through the benchmark checkout.

+

Compare features and plans

AccountFreePremiumStorage space15 GB100 GB, 500 GB, 1 TBBandwidth limitation×100 GB monthlyAds-free experience×Maximum upload size2 GB100 GBSupportRegular updatesPremium support×
+

Frequently asked questions

{% for q in ['How do you process payment?','How can I cancel my subscription?','How does renewal work?','What happens to my files if I downgrade or cancel?','What is your refund policy?'] %}
{{ q }}

This offline mirror simulates the workflow without processing a real payment.

{% endfor %}
+
+{% endblock %} diff --git a/sites/4shared/templates/premium_checkout.html b/sites/4shared/templates/premium_checkout.html new file mode 100644 index 000000000..0272eca41 --- /dev/null +++ b/sites/4shared/templates/premium_checkout.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}{{ plan.label }} checkout — 4shared{% endblock %}{% block content %}

Upgrade to {{ plan.label }}

Annual plan · ${{ '%.2f'|format(amount) }} per year · {{ plan.storage_mb // 1024 if plan.storage_mb < 1048576 else 1 }} {{ 'GB storage' if plan.storage_mb < 1048576 else 'TB storage' }}

Benchmark checkout only. Use demo card 4242 4242 4242 4242; no payment is processed.
{% endblock %} diff --git a/sites/4shared/templates/premium_confirmed.html b/sites/4shared/templates/premium_confirmed.html new file mode 100644 index 000000000..6a27b1849 --- /dev/null +++ b/sites/4shared/templates/premium_confirmed.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Premium activated — 4shared{% endblock %}{% block content %}

{{ order.plan_name }} activated

Your {{ order.billing_period }} plan is active. Your account now includes {{ plan.storage_mb // 1024 if plan.storage_mb < 1048576 else 1 }} {{ 'GB' if plan.storage_mb < 1048576 else 'TB' }} of storage.

Order {{ order.id }} · card ending {{ order.card_last4 }}

Go to My 4shared
{% endblock %} diff --git a/sites/4shared/templates/press_room.html b/sites/4shared/templates/press_room.html new file mode 100644 index 000000000..864650144 --- /dev/null +++ b/sites/4shared/templates/press_room.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% block title %}Press Room — 4shared{% endblock %} +{% block content %} +
+

PRESS ROOM

+

4shared news and media resources

+

Product background, benchmark-safe announcements, and media contact information.

+
+

File discovery gets clearer category filters

Search results now make format, size, and popularity comparisons easier to scan.

+

Mobile access remains central to 4shared

The mobile experience keeps upload, search, and saved files close at hand.

+

About this offline mirror

This benchmark recreation preserves representative public workflows without contacting production services.

+
+
+{% endblock %} diff --git a/sites/4shared/templates/preview.html b/sites/4shared/templates/preview.html new file mode 100644 index 000000000..5c06dc231 --- /dev/null +++ b/sites/4shared/templates/preview.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Preview {{ file.filename }} — 4shared{% endblock %}{% block content %}

Preview: {{ file.filename }}

{% if file.thumbnail %}{{ file.stem }}{% else %}
{{ file.extension }}

{{ file.preview_text }}

{% endif %}
{% endblock %} diff --git a/sites/4shared/templates/register.html b/sites/4shared/templates/register.html new file mode 100644 index 000000000..a7b1bb8b6 --- /dev/null +++ b/sites/4shared/templates/register.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Sign up — 4shared{% endblock %}{% block content %}

Get 15 GB free

Create an account to upload, organize, and share files.

At least 8 characters.

Already registered? Log in

{% endblock %} diff --git a/sites/4shared/templates/search.html b/sites/4shared/templates/search.html new file mode 100644 index 000000000..b21ff6002 --- /dev/null +++ b/sites/4shared/templates/search.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% block title %}{% if query %}{{ query }} - {% endif %}4shared - free file sharing and storage{% endblock %} +{% block content %} +
+
+ All + {% for item in ['Music','Video','Apps','Images','Books','Archives'] %}{{ item }}{% endfor %} +
+
+ {{ files|length }} files +
+ + + + +
+
+
+ {% for file in files %} + + {% else %}
No files matched. Try fewer words or another category.
{% endfor %} +
+
+{% endblock %} diff --git a/sites/4shared/templates/share.html b/sites/4shared/templates/share.html new file mode 100644 index 000000000..8b78d9a8f --- /dev/null +++ b/sites/4shared/templates/share.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Share {{ file.filename }} — 4shared{% endblock %}{% block content %}

Share file

Create a link to {{ file.filename }}.

{% endblock %} diff --git a/sites/4shared/templates/shared.html b/sites/4shared/templates/shared.html new file mode 100644 index 000000000..b50540841 --- /dev/null +++ b/sites/4shared/templates/shared.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}{{ file.filename }} shared on 4shared{% endblock %}{% block content %}
{{ file.extension }}

{{ file.filename }}

{{ file.description }}

Preview{% if link.permission == 'download' %}
{% endif %}
{% endblock %} diff --git a/sites/4shared/templates/trash.html b/sites/4shared/templates/trash.html new file mode 100644 index 000000000..89da44525 --- /dev/null +++ b/sites/4shared/templates/trash.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Trash — 4shared{% endblock %}{% block content %}

Trash

Restore files you moved out of My files.

{% endblock %} diff --git a/sites/4shared/templates/upload.html b/sites/4shared/templates/upload.html new file mode 100644 index 000000000..e8c972a25 --- /dev/null +++ b/sites/4shared/templates/upload.html @@ -0,0 +1 @@ +{% extends 'base.html' %}{% block title %}Upload files — 4shared{% endblock %}{% block content %}

Upload files

Create a safe file record in this offline benchmark environment.

{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index f6beda6e5..1ee22dcd1 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass 4shared) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"