diff --git a/.dockerignore b/.dockerignore index d870390d..08408d22 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,10 +5,12 @@ __pycache__/ .mypy_cache/ .pytest_cache/ .github/ -.git/ .pdm-build/ .pdm-python .eggs/ +.git/ +!.git/HEAD +!.git/refs/heads/* venv/ .venv/ @@ -16,6 +18,7 @@ venv/ .docker-venv/ node_modules/ +docs/ build/ dist/ brew_dist/ diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 7ed9d40d..9b95071e 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -25,36 +25,23 @@ jobs: dh-python debhelper devscripts dput software-properties-common \ python3-distutils python3-setuptools python3-wheel python3-stdeb - - name: Build Debian/Apt sdist_dsc - run: | - rm -Rf deb_dist/* - python3 setup.py --command-packages=stdeb.command sdist_dsc + # - name: Build Debian/Apt sdist_dsc + # run: | + # ./bin/build_pip.sh - - name: Build Debian/Apt bdist_deb - run: | - python3 setup.py --command-packages=stdeb.command bdist_deb + # - name: Check ArchiveBox version + # run: | + # # must create dir needed for snaps to run as non-root on github actions + # sudo mkdir -p /run/user/1001 && sudo chmod -R 777 /run/user/1001 + # mkdir "${{ github.workspace }}/data" && cd "${{ github.workspace }}/data" + # archivebox --version + # archivebox init --setup - - name: Install archivebox from deb - run: | - cd deb_dist/ - sudo apt-get install ./archivebox*.deb - cd .. - python3 -c 'from distutils.core import run_setup; result = run_setup("./setup.py", stop_after="init"); print("\n".join(result.install_requires + result.extras_require["sonic"]))' > ./requirements.txt - python3 -m pip install -r ./requirements.txt - - - name: Check ArchiveBox version - run: | - # must create dir needed for snaps to run as non-root on github actions - sudo mkdir -p /run/user/1001 && sudo chmod -R 777 /run/user/1001 - mkdir "${{ github.workspace }}/data" && cd "${{ github.workspace }}/data" - archivebox --version - archivebox init --setup - - - name: Add some links to test - run: | - cd "${{ github.workspace }}/data" - archivebox add 'https://example.com' - archivebox status + # - name: Add some links to test + # run: | + # cd "${{ github.workspace }}/data" + # archivebox add 'https://example.com' + # archivebox status # - name: Commit built package # run: | diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9840f7ae..75c7658c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,8 +11,7 @@ on: env: DOCKER_IMAGE: archivebox-ci - - + jobs: buildx: runs-on: ubuntu-latest @@ -60,13 +59,12 @@ jobs: uses: docker/metadata-action@v5 with: images: archivebox/archivebox,nikisweeting/archivebox - flavor: | - latest=auto tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha + type=raw,value=latest,enable={{is_default_branch}} - name: Build and push id: docker_build @@ -78,8 +76,18 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new platforms: linux/amd64,linux/arm64,linux/arm/v7 - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} + + # This ugly bit is necessary if you don't want your cache to grow forever + # until it hits GitHub's limit of 5GB. + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000..75786914 --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,58 @@ +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Build GitHub Pages website + +on: + # Runs on pushes targeting the default branch + push: + branches: ["dev"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 1 + - name: Copy README.md into place + run: | + rm ./website/README.md + cp ./README.md ./website/README.md + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./website + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index bfd16bcc..0c6aadfb 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,19 +19,26 @@ jobs: fetch-depth: 1 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: '3.11' architecture: x64 + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ripgrep build-essential python3-dev python3-setuptools libssl-dev libldap2-dev libsasl2-dev zlib1g-dev libatomic1 gnupg2 curl wget python3-ldap python3-msgpack python3-mutagen python3-regex python3-pycryptodome procps + version: 1.0 + - uses: pdm-project/setup-pdm@v3 + with: + python-version: '3.11' + cache: true - name: Install dependencies run: pdm install --fail-fast --no-lock --group :all --no-self - name: Build package run: | - rm ./dist/archivebox-*.whl pdm build - name: Install from build @@ -45,8 +52,8 @@ jobs: archivebox version archivebox status - - name: Publish package distributions to PyPI - run: pdm publish --no-build + #- name: Publish package distributions to PyPI + # run: pdm publish --no-build # - name: Push build to PyPI # run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a30133a..79cc28e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,11 +13,12 @@ jobs: strategy: matrix: - os: [ubuntu-20.04, macos-latest, windows-latest] - python: [3.9] + os: [ubuntu-22.04] + # os: [ubuntu-22.04, macos-latest, windows-latest] + python: [3.11] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 1 @@ -29,13 +30,16 @@ jobs: python-version: ${{ matrix.python }} architecture: x64 - - name: Set up Node JS 14.7.0 - uses: actions/setup-node@v3 + - name: Set up Node JS + uses: actions/setup-node@v4 with: - node-version: 18.12.0 + node-version: 20.10.0 - name: Setup PDM uses: pdm-project/setup-pdm@v3 + with: + python-version: '3.11' + cache: true ### Install Python & JS Dependencies - name: Get pip cache dir @@ -44,7 +48,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache-pip with: path: ${{ steps.pip-cache.outputs.dir }} @@ -52,11 +56,16 @@ jobs: restore-keys: | ${{ runner.os }}-${{ matrix.python }}-venv- + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ripgrep build-essential python3-dev python3-setuptools libssl-dev libldap2-dev libsasl2-dev zlib1g-dev libatomic1 python3-minimal gnupg2 curl wget python3-ldap python3-msgpack python3-mutagen python3-regex python3-pycryptodome procps + version: 1.0 + - name: Install pip dependencies run: | python -m pip install --upgrade pip setuptools wheel pytest bottle build - ./bin/build_pip.sh - pdm install + python -m pip install -r requirements.txt + python -m pip install -e .[sonic,ldap] - name: Get npm cache dir id: npm-cache @@ -64,7 +73,7 @@ jobs: echo "::set-output name=dir::$GITHUB_WORKSPACE/node_modules" - name: Cache npm - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache-npm with: path: ${{ steps.npm-cache.outputs.dir }} @@ -99,7 +108,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 1 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..7224eee9 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,34 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +submodules: + include: all + recursive: true + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + nodejs: "20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements.txt + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 10e153e3..97fd1770 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,7 +45,7 @@ ENV TZ=UTC \ # Version config ENV PYTHON_VERSION=3.11 \ - NODE_VERSION=21 + NODE_VERSION=20 # User config ENV ARCHIVEBOX_USER="archivebox" \ @@ -73,7 +73,8 @@ COPY --chown=root:root --chmod=755 package.json "$CODE_DIR/" RUN grep '"version": ' "${CODE_DIR}/package.json" | awk -F'"' '{print $4}' > /VERSION.txt # Force apt to leave downloaded binaries in /var/cache/apt (massively speeds up Docker builds) -RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ + && rm -f /etc/apt/apt.conf.d/docker-clean # Print debug info about build and save it to disk, for human eyes only, not used by anything else RUN (echo "[i] Docker build for ArchiveBox $(cat /VERSION.txt) starting..." \ @@ -123,7 +124,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$T RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$TARGETVARIANT --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-$TARGETARCH$TARGETVARIANT \ echo "[+] Installing Node $NODE_VERSION environment in $NODE_MODULES..." \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" >> /etc/apt/sources.list.d/nodejs.list \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && curl -fsSL "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" | gpg --dearmor | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ && apt-get update -qq \ && apt-get install -qq -y -t bookworm-backports --no-install-recommends \ nodejs libatomic1 python3-minimal \ @@ -171,10 +172,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$T # Save version info && ( \ which curl && curl --version | head -n1 \ - && which wget && wget --version | head -n1 \ - && which yt-dlp && yt-dlp --version | head -n1 \ - && which git && git --version | head -n1 \ - && which rg && rg --version | head -n1 \ + && which wget && wget --version 2>&1 | head -n1 \ + && which yt-dlp && yt-dlp --version 2>&1 | head -n1 \ + && which git && git --version 2>&1 | head -n1 \ + && which rg && rg --version 2>&1 | head -n1 \ && echo -e '\n\n' \ ) | tee -a /VERSION.txt @@ -202,13 +203,13 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$T && chown -R $ARCHIVEBOX_USER "$PLAYWRIGHT_BROWSERS_PATH" \ # Save version info && ( \ - which chromium-browser && /usr/bin/chromium-browser --version \ + which chromium-browser && /usr/bin/chromium-browser --version || /usr/lib/chromium/chromium --version \ && echo -e '\n\n' \ ) | tee -a /VERSION.txt # Install Node dependencies WORKDIR "$CODE_DIR" -COPY --chown=root:root --chmod=755 "package.json" "package-lock.json" "$CODE_DIR/" +COPY --chown=root:root --chmod=755 "package.json" "package-lock.json" "$CODE_DIR"/ RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-$TARGETARCH$TARGETVARIANT \ echo "[+] Installing NPM extractor dependencies from package.json into $NODE_MODULES..." \ && npm ci --prefer-offline --no-audit --cache /root/.npm \ @@ -222,9 +223,9 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-$TARGETARCH$TARGE # Install ArchiveBox Python dependencies WORKDIR "$CODE_DIR" -COPY --chown=root:root --chmod=755 "./pyproject.toml" "requirements.txt" "$CODE_DIR/" +COPY --chown=root:root --chmod=755 "./pyproject.toml" "requirements.txt" "$CODE_DIR"/ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$TARGETVARIANT --mount=type=cache,target=/root/.cache/pip,sharing=locked,id=pip-$TARGETARCH$TARGETVARIANT \ - echo "[+] Installing PIP ArchiveBox dependencies from requirements.txt for ${TARGETPLATFORM}..." \ + echo "[+] Installing PIP ArchiveBox dependencies from requirements.txt for ${TARGETPLATFORM}..." \ && apt-get update -qq \ && apt-get install -qq -y -t bookworm-backports --no-install-recommends \ build-essential \ @@ -239,7 +240,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$T && pip install -r requirements.txt \ && apt-get purge -y \ build-essential \ - # these are only needed to build CPython libs, we discard after build phase to shrink layer size && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* @@ -247,15 +247,15 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$T COPY --chown=root:root --chmod=755 "." "$CODE_DIR/" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$TARGETVARIANT --mount=type=cache,target=/root/.cache/pip,sharing=locked,id=pip-$TARGETARCH$TARGETVARIANT \ echo "[*] Installing PIP ArchiveBox package from $CODE_DIR..." \ - && apt-get update -qq \ + # && apt-get update -qq \ # install C compiler to build deps on platforms that dont have 32-bit wheels available on pypi - && apt-get install -qq -y -t bookworm-backports --no-install-recommends \ - build-essential \ + # && apt-get install -qq -y -t bookworm-backports --no-install-recommends \ + # build-essential \ # INSTALL ARCHIVEBOX python package globally from CODE_DIR, with all optional dependencies && pip install -e "$CODE_DIR"[sonic,ldap] \ # save docker image size and always remove compilers / build tools after building is complete - && apt-get purge -y build-essential \ - && apt-get autoremove -y \ + # && apt-get purge -y build-essential \ + # && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* #################################################### @@ -277,11 +277,10 @@ ENV IN_DOCKER=True # Print version for nice docker finish summary RUN (echo -e "\n\n[√] Finished Docker build succesfully. Saving build summary in: /VERSION.txt" \ - && echo -e "PLATFORM=${TARGETPLATFORM} ARCH=$(uname -m) ($(uname -s) ${TARGETARCH} ${TARGETVARIANT})" \ - && echo -e "BUILD_END_TIME=$(date +"%Y-%m-%d %H:%M:%S %s") TZ=${TZ}\n\n" \ - && "$CODE_DIR/bin/docker_entrypoint.sh" \ - archivebox version 2>&1 \ + && echo -e "PLATFORM=${TARGETPLATFORM} ARCH=$(uname -m) ($(uname -s) ${TARGETARCH} ${TARGETVARIANT})\n" \ + && echo -e "BUILD_END_TIME=$(date +"%Y-%m-%d %H:%M:%S %s")\n\n" \ ) | tee -a /VERSION.txt +RUN "$CODE_DIR"/bin/docker_entrypoint.sh version 2>&1 | tee -a /VERSION.txt #################################################### diff --git a/README.md b/README.md index ff3f57c7..91b56c22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

ArchiveBox
Open-source self-hosted web archiving.


@@ -9,8 +9,7 @@ GitHub | Documentation | Info & Motivation | -Community | -Roadmap +Community
@@ -18,7 +17,7 @@ -   +   @@ -33,9 +32,9 @@ curl -sSL 'https://get.archivebox.io' | sh # (or see pip/brew/Docker instruct **ArchiveBox is a powerful, self-hosted internet archiving solution to collect, save, and view websites offline.** -Without active preservation effort, everything on the internet eventually dissapears or degrades. Archive.org does a great job as a free central archive, but they require all archives to be public, and they cant save every type of content. +Without active preservation effort, everything on the internet eventually dissapears or degrades. Archive.org does a great job as a free central archive, but they require all archives to be public, and they can't save every type of content. -*ArchiveBox is an open source tool that helps you archive web content on your own (or privately within an organization): save sharable copies of browser bookmarks, preserve evidence for legal cases, backup photos on FB / Insta / Flickr, download your media from YT / Soundcloud / etc., snapshot research papers in academic citations, and more...* +*ArchiveBox is an open source tool that helps you archive web content on your own (or privately within an organization): save copies of browser bookmarks, preserve evidence for legal cases, backup photos from FB / Insta / Flickr, download your media from YT / Soundcloud / etc., snapshot research papers & academic citations, and more...* > ➡️ *Use ArchiveBox as a [command-line package](#quickstart) and/or [self-hosted web app](#quickstart) on Linux, macOS, or in [Docker](#quickstart).* @@ -72,7 +71,7 @@ The goal is to sleep soundly knowing the part of the internet you care about wil


-bookshelf graphic   logo   bookshelf graphic +bookshelf graphic   logo   bookshelf graphic

Demo | Screenshots | Usage
@@ -110,10 +109,10 @@ ls ./archive/*/index.json # or browse directly via the filesyste


-cli init screenshot -cli init screenshot -server snapshot admin screenshot -server snapshot details page screenshot +cli init screenshot +cli init screenshot +server snapshot admin screenshot +server snapshot details page screenshot

@@ -146,7 +145,7 @@ ls ./archive/*/index.json # or browse directly via the filesyste

-grassgrass +grassgrass
# Quickstart @@ -320,6 +319,10 @@ See the pip-archive
Arch pacman / FreeBSD pkg / Nix nix (Arch/FreeBSD/NixOS/more)
+ +> [!WARNING] +> *These are contributed by external volunteers and may lag behind the official `pip` channel.* +
- +
✨ Alpha (contributors wanted!): for more info, see the: Electron ArchiveBox repo.
@@ -366,6 +369,7 @@ See below for usage examples using the CLI, W
Other providers of paid ArchiveBox hosting (not officially endorsed):


+
  • (USD $29-250/mo, pricing)
  • (from USD $2.6/mo)
  • @@ -438,7 +442,7 @@ ls ./archive/*/index.html # or inspect snapshots on the filesystem
    -grassgrass +grassgrass

    @@ -455,7 +459,7 @@ ls ./archive/*/index.html # or inspect snapshots on the filesystem ---
    -lego +lego

    @@ -469,12 +473,12 @@ ArchiveBox supports many input formats for URLs, including Pocket & Pinboard exp *Click these links for instructions on how to prepare your links from these sources:* -- TXT, RSS, XML, JSON, CSV, SQL, HTML, Markdown, or [any other text-based format...](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Import-a-list-of-URLs-from-a-text-file) -- [Browser history](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) or [browser bookmarks](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) (see instructions for: [Chrome](https://support.google.com/chrome/answer/96816?hl=en), [Firefox](https://support.mozilla.org/en-US/kb/export-firefox-bookmarks-to-backup-or-transfer), [Safari](http://i.imgur.com/AtcvUZA.png), [IE](https://support.microsoft.com/en-us/help/211089/how-to-import-and-export-the-internet-explorer-favorites-folder-to-a-32-bit-version-of-windows), [Opera](http://help.opera.com/Windows/12.10/en/importexport.html), [and more...](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive)) -- Browser extension [`archivebox-exporter`](https://github.com/tjhorner/archivebox-exporter) (realtime archiving from Chrome/Chromium/Firefox) +- TXT, RSS, XML, JSON, CSV, SQL, HTML, Markdown, or [any other text-based format...](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Import-a-list-of-URLs-from-a-text-file) +- [Browser history](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) or [browser bookmarks](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) (see instructions for: [Chrome](https://support.google.com/chrome/answer/96816?hl=en), [Firefox](https://support.mozilla.org/en-US/kb/export-firefox-bookmarks-to-backup-or-transfer), [Safari](https://github.com/ArchiveBox/ArchiveBox/assets/511499/24ad068e-0fa6-41f4-a7ff-4c26fc91f71a), [IE](https://support.microsoft.com/en-us/help/211089/how-to-import-and-export-the-internet-explorer-favorites-folder-to-a-32-bit-version-of-windows), [Opera](https://help.opera.com/en/latest/features/#bookmarks:~:text=Click%20the%20import/-,export%20button,-on%20the%20bottom), [and more...](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive)) +- Browser extension [`archivebox-exporter`](https://github.com/tjhorner/archivebox-exporter) (realtime archiving from Chrome/Chromium/Firefox) - [Pocket](https://getpocket.com/export), [Pinboard](https://pinboard.in/export/), [Instapaper](https://www.instapaper.com/user), [Shaarli](https://shaarli.readthedocs.io/en/master/Usage/#importexport), [Delicious](https://www.groovypost.com/howto/howto/export-delicious-bookmarks-xml/), [Reddit Saved](https://github.com/csu/export-saved-reddit), [Wallabag](https://doc.wallabag.org/en/user/import/wallabagv2.html), [Unmark.it](http://help.unmark.it/import-export), [OneTab](https://www.addictivetips.com/web/onetab-save-close-all-chrome-tabs-to-restore-export-or-import/), [and more...](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) - + ```bash @@ -501,7 +505,7 @@ It also includes a built-in scheduled import feature with `archivebox schedule` Inside each Snapshot folder, ArchiveBox saves these different types of extractor outputs as plain files: - + `./archive//*` @@ -525,7 +529,7 @@ It does everything out-of-the-box by default, but you can disable or tweak [indi ## Configuration - + ArchiveBox can be configured via environment variables, by using the `archivebox config` CLI, or by editing the `ArchiveBox.conf` config file directly. @@ -559,17 +563,29 @@ MAX_MEDIA_SIZE=1500m # default: 750m raise/lower youtubedl output size PUBLIC_INDEX=True # default: True whether anon users can view index PUBLIC_SNAPSHOTS=True # default: True whether anon users can view pages PUBLIC_ADD_VIEW=False # default: False whether anon users can add new URLs + +CHROME_USER_AGENT="Mozilla/5.0 ..." # change these to get around bot blocking +WGET_USER_AGENT="Mozilla/5.0 ..." +CURL_USER_AGENT="Mozilla/5.0 ..." ```
    ## Dependencies +To achieve high-fidelity archives in as many situations as possible, ArchiveBox depends on a variety of high-quality 3rd-party tools and libraries that specialize in extracting different types of content. + +
    +
    +Expand to learn more about ArchiveBox's dependencies... +
    + For better security, easier updating, and to avoid polluting your host system with extra dependencies, **it is strongly recommended to use the official [Docker image](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker)** with everything pre-installed for the best experience. -To achieve high-fidelity archives in as many situations as possible, ArchiveBox depends on a variety of 3rd-party tools and libraries that specialize in extracting different types of content. These optional dependencies used for archiving sites include: +These optional dependencies used for archiving sites include: + +archivebox --version CLI output screenshot showing dependencies installed - - `chromium` / `chrome` (for screenshots, PDF, DOM HTML, and headless JS scripts) - `node` & `npm` (for readability, mercury, and singlefile) @@ -593,21 +609,41 @@ archivebox --version # see info and check validity of installed dependencies Installing directly on **Windows without Docker or WSL/WSL2/Cygwin is not officially supported** (I cannot respond to Windows support tickets), but some advanced users have reported getting it working. -For detailed information about upgrading ArchiveBox and its dependencies, see: https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Install#dependencies +- https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install +- https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives +- https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#installing + +

    ## Archive Layout -All of ArchiveBox's state (including the index, snapshot data, and config file) is stored in a single folder called the "ArchiveBox data folder". All `archivebox` CLI commands must be run from inside this folder, and you first create it by running `archivebox init`. +All of ArchiveBox's state (including the SQLite DB, archived assets, config, logs, etc.) is stored in a single folder called the "ArchiveBox Data Folder". +Data folders can be created anywhere (`~/archivebox` or `$PWD/data` as seen in our examples), and you can create more than one for different collections. -The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard `index.sqlite3` database in the root of the data folder (it can also be exported as static JSON/HTML), and the archive snapshots are organized by date-added timestamp in the `./archive/` subfolder. +
    +
    +Expand to learn more about the layout of Archivebox's data on-disk... +
    + +All `archivebox` CLI commands are designed to be run from inside an ArchiveBox data folder, starting with `archivebox init` to initialize a new collection inside an empty directory. + +```bash +mkdir ~/archivebox && cd ~/archivebox # just an example, can be anywhere +archivebox init +``` + +The on-disk layout is optimized to be easy to browse by hand and durable long-term. The main index is a standard `index.sqlite3` database in the root of the data folder (it can also be [exported as static JSON/HTML](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive#2-export-and-host-it-as-static-html)), and the archive snapshots are organized by date-added timestamp in the `./archive/` subfolder. ```bash -./ +/data/ index.sqlite3 ArchiveBox.conf archive/ @@ -624,12 +660,26 @@ The on-disk layout is optimized to be easy to browse by hand and durable long-te Each snapshot subfolder `./archive//` includes a static `index.json` and `index.html` describing its contents, and the snapshot extractor outputs are plain files within the folder. +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Disk-Layout +- https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#large-archives +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#output-folder +- https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive +- https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives + +

    ## Static Archive Exporting -You can export the main index to browse it statically without needing to run a server. +You can export the main index to browse it statically as plain HTML files in a folder (without needing to run a server). + +
    +
    +Expand to learn how to export your ArchiveBox collection... +
    > **Note** > These exports are not paginated, exporting many URLs or the entire archive at once may be slow. Use the filtering CLI flags on the `archivebox list` command to export specific Snapshots or ranges. @@ -646,6 +696,14 @@ archivebox list --csv=timestamp,url,title > index.csv # export to csv spreadshe The paths in the static exports are relative, make sure to keep them next to your `./archive` folder when backing them up or viewing them. +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive#2-export-and-host-it-as-static-html +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#publishing +- https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#public_index--public_snapshots--public_add_view + + +

    @@ -663,6 +721,11 @@ The paths in the static exports are relative, make sure to keep them next to you If you're importing pages with private content or URLs containing secret tokens you don't want public (e.g Google Docs, paywalled content, unlisted videos, etc.), **you may want to disable some of the extractor methods to avoid leaking that content to 3rd party APIs or the public**. +
    +
    +Click to expand... + + ```bash # don't save private content to ArchiveBox, e.g.: archivebox add 'https://docs.google.com/document/d/12345somePrivateDocument' @@ -681,10 +744,27 @@ archivebox config --set SAVE_FAVICON=False # disable favicon fetching ( archivebox config --set CHROME_BINARY=chromium # ensure it's using Chromium instead of Chrome ``` +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview +- https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install#setting-up-a-chromium-user-profile +- https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#chrome_user_data_dir +- https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#cookies_file + +
    +
    + + ### Security Risks of Viewing Archived JS Be aware that malicious archived JS can access the contents of other pages in your archive when viewed. Because the Web UI serves all viewed snapshots from a single domain, they share a request context and **typical CSRF/CORS/XSS/CSP protections do not work to prevent cross-site request attacks**. See the [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#stealth-mode) page and [Issue #239](https://github.com/ArchiveBox/ArchiveBox/issues/239) for more details. +
    +
    +Click to expand... + + ```bash # visiting an archived page with malicious JS: https://127.0.0.1:8000/archive/1602401954/example.com/index.html @@ -699,9 +779,48 @@ The admin UI is also served from the same origin as replayed JS, so malicious pa *Note: Only the `wget` & `dom` extractor methods execute archived JS when viewing snapshots, all other archive methods produce static output that does not execute JS on viewing. If you are worried about these issues ^ you should disable these extractors using `archivebox config --set SAVE_WGET=False SAVE_DOM=False`.* +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview +- https://github.com/ArchiveBox/ArchiveBox/issues/239 +- https://github.com/ArchiveBox/ArchiveBox/security/advisories/GHSA-cr45-98w9-gwqx (`CVE-2023-45815`) +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#publishing + + +
    +
    + +### Working Around Sites that Block Archiving + +For various reasons, many large sites (Reddit, Twitter, Cloudflare, etc.) actively block archiving or bots in general. There are a number of approaches to work around this. + +
    +
    +Click to expand... +
    + +- Set [`CHROME_USER_AGENT`, `WGET_USER_AGENT`, `CURL_USER_AGENT`](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#curl_user_agent) to impersonate a real browser (instead of an ArchiveBox bot) +- Set up a logged-in browser session for archiving using [`CHROME_DATA_DIR` & `COOKIES_FILE`](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install#setting-up-a-chromium-user-profile) +- Rewrite your URLs before archiving to swap in an alternative frontend thats more bot-friendly e.g. + `reddit.com/some/url` -> `teddit.net/some/url`: https://github.com/mendel5/alternative-front-ends + + +In the future we plan on adding support for running JS scripts during archiving to block ads, cookie popups, modals, and fix other issues. Follow here for progress: [Issue #51](https://github.com/ArchiveBox/ArchiveBox/issues/51). + +
    +
    + + ### Saving Multiple Snapshots of a Single URL -First-class support for saving multiple snapshots of each site over time will be [added eventually](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). For now **ArchiveBox is designed to only archive each unique URL with each extractor type once**. The workaround to take multiple snapshots of the same URL is to make them slightly different by adding a hash: +ArchiveBox appends a hash with the current date `https://example.com#2020-10-24` to differentiate when a single URL is archived multiple times. + +
    +
    +Click to expand... +
    + +Because ArchiveBox uniquely identifies snapshots by URL, it must use a workaround to take multiple snapshots of the same URL (otherwise they would show up as a single Snapshot entry). It makes the URLs of repeated snapshots unique by adding a hash with the archive date at the end: ```bash archivebox add 'https://example.com#2020-10-24' @@ -709,14 +828,47 @@ archivebox add 'https://example.com#2020-10-24' archivebox add 'https://example.com#2020-10-25' ``` -The Re-Snapshot Button button in the Admin UI is a shortcut for this hash-date workaround. +The Re-Snapshot Button button in the Admin UI is a shortcut for this hash-date multi-snapshotting workaround. + +Improved support for saving multiple snapshots of a single URL without this hash-date workaround will be [added eventually](https://github.com/ArchiveBox/ArchiveBox/issues/179) (along with the ability to view diffs of the changes between runs). + +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/issues/179 +- https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#explanation-of-buttons-in-the-web-ui---admin-snapshots-list + + +
    +
    ### Storage Requirements -Because ArchiveBox is designed to ingest a firehose of browser history and bookmark feeds to a local disk, it can be much more disk-space intensive than a centralized service like the Internet Archive or Archive.today. **ArchiveBox can use anywhere from ~1gb per 1000 articles, to ~50gb per 1000 articles**, mostly dependent on whether you're saving audio & video using `SAVE_MEDIA=True` and whether you lower `MEDIA_MAX_SIZE=750mb`. +Because ArchiveBox is designed to ingest a large volume of URLs with multiple copies of each URL stored by different 3rd-party tools, it can be quite disk-space intensive. +There also also some special requirements when using filesystems like NFS/SMB/FUSE. + +
    +
    +Click to expand... +
    + +**ArchiveBox can use anywhere from ~1gb per 1000 articles, to ~50gb per 1000 articles**, mostly dependent on whether you're saving audio & video using `SAVE_MEDIA=True` and whether you lower `MEDIA_MAX_SIZE=750mb`. Disk usage can be reduced by using a compressed/deduplicated filesystem like ZFS/BTRFS, or by turning off extractors methods you don't need. You can also deduplicate content with a tool like [fdupes](https://github.com/adrianlopezroche/fdupes) or [rdfind](https://github.com/pauldreik/rdfind). **Don't store large collections on older filesystems like EXT3/FAT** as they may not be able to handle more than 50k directory entries in the `archive/` folder. **Try to keep the `index.sqlite3` file on local drive (not a network mount)** or SSD for maximum performance, however the `archive/` folder can be on a network mount or slower HDD. +If using Docker or NFS/SMB/FUSE for the `data/archive/` folder, you may need to set [`PUID` & `PGID`](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#puid--pgid) and [disable `root_squash`](https://github.com/ArchiveBox/ArchiveBox/issues/1304) on your fileshare server. + + +#### Learn More + +- https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Disk-Layout +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#output-folder +- https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#large-archives +- https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#puid--pgid +- https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#do-not-run-as-root + + + +

    --- @@ -726,36 +878,36 @@ Disk usage can be reduced by using a compressed/deduplicated filesystem like ZFS ## Screenshots
    - + @@ -768,20 +920,25 @@ Disk usage can be reduced by using a compressed/deduplicated filesystem like ZFS
    -paisley graphic +paisley graphic
    # Background & Motivation ArchiveBox aims to enable more of the internet to be saved from deterioration by empowering people to self-host their own archives. The intent is for all the web content you care about to be viewable with common software in 50 - 100 years without needing to run ArchiveBox or other specialized software to replay it. +
    +
    +Click to read more... +
    + Vast treasure troves of knowledge are lost every day on the internet to link rot. As a society, we have an imperative to preserve some important parts of that treasure, just like we preserve our books, paintings, and music in physical libraries long after the originals go out of print or fade into obscurity. Whether it's to resist censorship by saving articles before they get taken down or edited, or just to save a collection of early 2010's flash games you love to play, having the tools to archive internet content enables to you save the stuff you care most about before it disappears.
    -
    - Image from WTF is Link Rot?...
    +
    + Image from Perma.cc...
    The balance between the permanence and ephemeral nature of content on the internet is part of what makes it beautiful. I don't think everything should be preserved in an automated fashion--making all content permanent and never removable, but I do think people should be able to decide for themselves and effectively archive specific content that they care about. @@ -789,11 +946,16 @@ The balance between the permanence and ephemeral nature of content on the intern Because modern websites are complicated and often rely on dynamic content, ArchiveBox archives the sites in **several different formats** beyond what public archiving services like Archive.org/Archive.is save. Using multiple methods and the market-dominant browser to execute JS ensures we can save even the most complex, finicky websites in at least a few high-quality, long-term data formats. +
    +
    + ## Comparison to Other Projects -comparison +comparison -▶ **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** + +> [!TIP] +> **Check out our [community page](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) for an index of web archiving initiatives and projects.** A variety of open and closed-source archiving projects exist, but few provide a nice UI and CLI to manage a large, high-fidelity archive collection over time. @@ -809,7 +971,10 @@ By having each user store their own content locally, we can save much larger por ArchiveBox differentiates itself from [similar self-hosted projects](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects) by providing both a comprehensive CLI interface for managing your archive, a Web UI that can be used either independently or together with the CLI, and a simple on-disk data format that can be used without either. -ArchiveBox is neither the highest fidelity nor the simplest tool available for self-hosted archiving, rather it's a jack-of-all-trades that tries to do most things well by default. It can be as simple or advanced as you want, and is designed to do everything out-of-the-box but be tuned to suit your needs. +
    +Click to see the ⭐️ officially recommended alternatives to ArchiveBox... +
    + *If you want better fidelity for very complex interactive pages with heavy JS/streams/API requests, check out [ArchiveWeb.page](https://archiveweb.page) and [ReplayWeb.page](https://replayweb.page).* @@ -819,16 +984,23 @@ ArchiveBox is neither the highest fidelity nor the simplest tool available for s For more alternatives, see our [list here](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#Web-Archiving-Projects)... +ArchiveBox is neither the highest fidelity nor the simplest tool available for self-hosted archiving, rather it's a jack-of-all-trades that tries to do most things well by default. We encourage you to try these other tools made by our friends if ArchiveBox isn't suited to your needs. + +
    + +
    +

    -dependencies graphic +dependencies graphic
    ## Internet Archiving Ecosystem Whether you want to learn which organizations are the big players in the web archiving space, want to find a specific open-source tool for your web archiving need, or just want to see where archivists hang out online, our Community Wiki page serves as an index of the broader web archiving community. Check it out to learn about some of the coolest web archiving projects and communities on the web! - + + - [Community Wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) - [The Master Lists](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community#the-master-lists) @@ -843,6 +1015,7 @@ Whether you want to learn which organizations are the big players in the web arc - Learn why archiving the internet is important by reading the "[On the Importance of Web Archiving](https://items.ssrc.org/parameters/on-the-importance-of-web-archiving/)" blog post. - Reach out to me for questions and comments via [@ArchiveBoxApp](https://twitter.com/ArchiveBoxApp) or [@theSquashSH](https://twitter.com/thesquashSH) on Twitter +
    **Need help building a custom archiving solution?** @@ -856,7 +1029,7 @@ Whether you want to learn which organizations are the big players in the web arc ---
    -documentation graphic +documentation graphic
    # Documentation @@ -872,27 +1045,32 @@ You can also access the docs locally by looking in the [`ArchiveBox/docs/`](http - [Quickstart](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart) - [Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Install) - [Docker](https://github.com/ArchiveBox/ArchiveBox/wiki/Docker) - -## Reference - - [Usage](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage) - [Configuration](https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration) - [Supported Sources](https://github.com/ArchiveBox/ArchiveBox/wiki/Quickstart#2-get-your-list-of-urls-to-archive) - [Supported Outputs](https://github.com/ArchiveBox/ArchiveBox/wiki#can-save-these-things-for-each-site) + +## Advanced + +- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) - [Scheduled Archiving](https://github.com/ArchiveBox/ArchiveBox/wiki/Scheduled-Archiving) - [Publishing Your Archive](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive) - [Chromium Install](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install) +- [Cookies & Sessions Setup](https://github.com/ArchiveBox/ArchiveBox/wiki/Chromium-Install#setting-up-a-chromium-user-profile) - [Security Overview](https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview) -- [Troubleshooting](https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting) - [Upgrading or Merging Archives](https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives) + +## Developers + +- [Developer Documentation](https://github.com/ArchiveBox/ArchiveBox#archivebox-development) - [Python API](https://docs.archivebox.io/en/latest/modules.html) (alpha) - [REST API](https://github.com/ArchiveBox/ArchiveBox/issues/496) (alpha) ## More Info -- [Tickets](https://github.com/ArchiveBox/ArchiveBox/issues) +- [Bug Tracker](https://github.com/ArchiveBox/ArchiveBox/issues) - [Roadmap](https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap) -- [Changelog](https://github.com/ArchiveBox/ArchiveBox/wiki/Changelog) +- [Changelog](https://github.com/ArchiveBox/ArchiveBox/releases) - [Donations](https://github.com/ArchiveBox/ArchiveBox/wiki/Donations) - [Background & Motivation](https://github.com/ArchiveBox/ArchiveBox#background--motivation) - [Web Archiving Community](https://github.com/ArchiveBox/ArchiveBox/wiki/Web-Archiving-Community) @@ -902,7 +1080,7 @@ You can also access the docs locally by looking in the [`ArchiveBox/docs/`](http ---
    -development +development
    # ArchiveBox Development @@ -989,7 +1167,21 @@ https://stackoverflow.com/questions/1074212/how-can-i-see-the-raw-sql-queries-dj
    Click to expand... -```bash| +##### Use a Pre-Built Image + +If you're looking for the latest `dev` Docker image, it's often available pre-built on Docker Hub, simply pull and use `archivebox/archivebox:dev`. + +```bash +docker pull archivebox/archivebox:dev +docker run archivebox/archivebox:dev version +# verify the BUILD_TIME and COMMIT_HASH in the output are recent +``` + +##### Build Branch from Source + +You can also build and run any branch yourself from source, for example to build & use `dev` locally: + +```bash # docker-compose.yml: services: archivebox: @@ -997,11 +1189,11 @@ services: build: 'https://github.com/ArchiveBox/ArchiveBox.git#dev' ... -# docker: +# or with plain Docker: docker build -t archivebox:dev https://github.com/ArchiveBox/ArchiveBox.git#dev docker run -it -v $PWD:/data archivebox:dev init --setup -# bare metal: +# or with pip: pip install 'git+https://github.com/pirate/ArchiveBox@dev' npm install 'git+https://github.com/ArchiveBox/ArchiveBox.git#dev' archivebox init --setup diff --git a/archivebox/cli/archivebox_schedule.py b/archivebox/cli/archivebox_schedule.py index d4747906..f606979b 100644 --- a/archivebox/cli/archivebox_schedule.py +++ b/archivebox/cli/archivebox_schedule.py @@ -39,6 +39,12 @@ def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional default=None, help='Run ArchiveBox once every [timeperiod] (hour/day/month/year or cron format e.g. "0 0 * * *")', ) + parser.add_argument( + '--tag', '-t', + type=str, + default='', + help="Tag the added URLs with the provided tags e.g. --tag=tag1,tag2,tag3", + ) parser.add_argument( '--depth', # '-d', type=int, @@ -97,6 +103,7 @@ def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional run_all=command.run_all, quiet=command.quiet, every=command.every, + tag=command.tag, depth=command.depth, overwrite=command.overwrite, update=command.update, diff --git a/archivebox/config.py b/archivebox/config.py index 99a1847c..c4a3aef6 100644 --- a/archivebox/config.py +++ b/archivebox/config.py @@ -30,6 +30,7 @@ import inspect import getpass import platform import shutil +import requests import django from sqlite3 import dbapi2 as sqlite3 @@ -51,25 +52,6 @@ from .config_stubs import ( ) -### Pre-Fetch Minimal System Config - -SYSTEM_USER = getpass.getuser() or os.getlogin() - -try: - import pwd - SYSTEM_USER = pwd.getpwuid(os.geteuid()).pw_name or SYSTEM_USER -except KeyError: - # Process' UID might not map to a user in cases such as running the Docker image - # (where `archivebox` is 999) as a different UID. - pass -except ModuleNotFoundError: - # pwd is only needed for some linux systems, doesn't exist on windows - pass -except Exception: - # this should never happen, uncomment to debug - # raise - pass - ############################### Config Schema ################################## CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { @@ -81,7 +63,6 @@ CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { 'IN_QEMU': {'type': bool, 'default': False}, 'PUID': {'type': int, 'default': os.getuid()}, 'PGID': {'type': int, 'default': os.getgid()}, - # TODO: 'SHOW_HINTS': {'type: bool, 'default': True}, }, 'GENERAL_CONFIG': { @@ -257,7 +238,7 @@ CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = { 'POCKET_CONSUMER_KEY': {'type': str, 'default': None}, 'POCKET_ACCESS_TOKENS': {'type': dict, 'default': {}}, - 'READWISE_READER_TOKENS': {'type': dict, 'default': {}}, + 'READWISE_READER_TOKENS': {'type': dict, 'default': {}}, }, } @@ -376,23 +357,126 @@ ALLOWED_IN_OUTPUT_DIR = { 'static_index.json', } -def get_version(config): - return importlib.metadata.version(__package__ or 'archivebox') - -def get_commit_hash(config): - try: - return list((config['PACKAGE_DIR'] / '../.git/refs/heads/').glob('*'))[0].read_text().strip() - except Exception: - return None - -############################## Derived Config ################################## - ALLOWDENYLIST_REGEX_FLAGS: int = re.IGNORECASE | re.UNICODE | re.MULTILINE + +############################## Version Config ################################## + +def get_system_user(): + SYSTEM_USER = getpass.getuser() or os.getlogin() + try: + import pwd + return pwd.getpwuid(os.geteuid()).pw_name or SYSTEM_USER + except KeyError: + # Process' UID might not map to a user in cases such as running the Docker image + # (where `archivebox` is 999) as a different UID. + pass + except ModuleNotFoundError: + # pwd doesn't exist on windows + pass + except Exception: + # this should never happen, uncomment to debug + # raise + pass + + return SYSTEM_USER + +def get_version(config): + try: + return importlib.metadata.version(__package__ or 'archivebox') + except importlib.metadata.PackageNotFoundError: + try: + pyproject_config = (config['PACKAGE_DIR'] / 'pyproject.toml').read_text() + for line in pyproject_config: + if line.startswith('version = '): + return line.split(' = ', 1)[-1].strip('"') + except FileNotFoundError: + # building docs, pyproject.toml is not available + return 'dev' + + raise Exception('Failed to detect installed archivebox version!') + +def get_commit_hash(config) -> Optional[str]: + try: + git_dir = config['PACKAGE_DIR'] / '../.git' + ref = (git_dir / 'HEAD').read_text().strip().split(' ')[-1] + commit_hash = git_dir.joinpath(ref).read_text().strip() + return commit_hash + except Exception: + pass + + try: + return list((config['PACKAGE_DIR'] / '../.git/refs/heads/').glob('*'))[0].read_text().strip() + except Exception: + pass + + return None + +def get_build_time(config) -> str: + if config['IN_DOCKER']: + docker_build_end_time = Path('/VERSION.txt').read_text().rsplit('BUILD_END_TIME=')[-1].split('\n', 1)[0] + return docker_build_end_time + + src_last_modified_unix_timestamp = (config['PACKAGE_DIR'] / 'config.py').stat().st_mtime + return datetime.fromtimestamp(src_last_modified_unix_timestamp).strftime('%Y-%m-%d %H:%M:%S %s') + +def get_versions_available_on_github(config): + """ + returns a dictionary containing the ArchiveBox GitHub release info for + the recommended upgrade version and the currently installed version + """ + + # we only want to perform the (relatively expensive) check for new versions + # when its most relevant, e.g. when the user runs a long-running command + subcommand_run_by_user = sys.argv[3] if len(sys.argv) > 3 else 'help' + long_running_commands = ('add', 'schedule', 'update', 'status', 'server') + if subcommand_run_by_user not in long_running_commands: + return None + + github_releases_api = "https://api.github.com/repos/ArchiveBox/ArchiveBox/releases" + response = requests.get(github_releases_api) + if response.status_code != 200: + stderr(f'[!] Warning: GitHub API call to check for new ArchiveBox version failed! (status={response.status_code})', color='lightyellow', config=config) + return None + all_releases = response.json() + + installed_version = parse_version_string(config['VERSION']) + + # find current version or nearest older version (to link to) + current_version = None + for idx, release in enumerate(all_releases): + release_version = parse_version_string(release['tag_name']) + if release_version <= installed_version: + current_version = release + break + + current_version = current_version or all_releases[-1] + + # recommended version is whatever comes after current_version in the release list + # (perhaps too conservative to only recommend upgrading one version at a time, but it's safest) + try: + recommended_version = all_releases[idx+1] + except IndexError: + recommended_version = None + + return {'recommended_version': recommended_version, 'current_version': current_version} + +def can_upgrade(config): + if config['VERSIONS_AVAILABLE'] and config['VERSIONS_AVAILABLE']['recommended_version']: + recommended_version = parse_version_string(config['VERSIONS_AVAILABLE']['recommended_version']['tag_name']) + current_version = parse_version_string(config['VERSIONS_AVAILABLE']['current_version']['tag_name']) + return recommended_version > current_version + return False + + +############################## Derived Config ################################## + +# These are derived/computed values calculated *after* all user-provided config values are ingested +# they appear in `archivebox config` output and are intended to be read-only for the user DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'TERM_WIDTH': {'default': lambda c: lambda: shutil.get_terminal_size((100, 10)).columns}, - 'USER': {'default': lambda c: SYSTEM_USER}, + 'USER': {'default': lambda c: get_system_user()}, 'ANSI': {'default': lambda c: DEFAULT_CLI_COLORS if c['USE_COLOR'] else {k: '' for k in DEFAULT_CLI_COLORS.keys()}}, 'PACKAGE_DIR': {'default': lambda c: Path(__file__).resolve().parent}, @@ -408,12 +492,17 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'CHROME_USER_DATA_DIR': {'default': lambda c: find_chrome_data_dir() if c['CHROME_USER_DATA_DIR'] is None else (Path(c['CHROME_USER_DATA_DIR']).resolve() if c['CHROME_USER_DATA_DIR'] else None)}, # None means unset, so we autodetect it with find_chrome_Data_dir(), but emptystring '' means user manually set it to '', and we should store it as None 'URL_DENYLIST_PTN': {'default': lambda c: c['URL_DENYLIST'] and re.compile(c['URL_DENYLIST'] or '', ALLOWDENYLIST_REGEX_FLAGS)}, 'URL_ALLOWLIST_PTN': {'default': lambda c: c['URL_ALLOWLIST'] and re.compile(c['URL_ALLOWLIST'] or '', ALLOWDENYLIST_REGEX_FLAGS)}, - 'DIR_OUTPUT_PERMISSIONS': {'default': lambda c: c['OUTPUT_PERMISSIONS'].replace('6', '7').replace('4', '5')}, + 'DIR_OUTPUT_PERMISSIONS': {'default': lambda c: c['OUTPUT_PERMISSIONS'].replace('6', '7').replace('4', '5')}, # exec is always needed to list directories 'ARCHIVEBOX_BINARY': {'default': lambda c: sys.argv[0] or bin_path('archivebox')}, - 'VERSION': {'default': lambda c: get_version(c)}, - 'COMMIT_HASH': {'default': lambda c: get_commit_hash(c)}, + + 'VERSION': {'default': lambda c: get_version(c).split('+', 1)[0]}, # remove +editable from user-displayed version string + 'COMMIT_HASH': {'default': lambda c: get_commit_hash(c)}, # short git commit hash of codebase HEAD commit + 'BUILD_TIME': {'default': lambda c: get_build_time(c)}, # docker build completed time or python src last modified time + 'VERSIONS_AVAILABLE': {'default': lambda c: get_versions_available_on_github(c)}, + 'CAN_UPGRADE': {'default': lambda c: can_upgrade(c)}, + 'PYTHON_BINARY': {'default': lambda c: sys.executable}, 'PYTHON_ENCODING': {'default': lambda c: sys.stdout.encoding.upper()}, 'PYTHON_VERSION': {'default': lambda c: '{}.{}.{}'.format(*sys.version_info[:3])}, @@ -423,7 +512,7 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'SQLITE_BINARY': {'default': lambda c: inspect.getfile(sqlite3)}, 'SQLITE_VERSION': {'default': lambda c: sqlite3.version}, - #'SQLITE_JOURNAL_MODE': {'default': lambda c: 'wal'}, # set at runtime below, interesting but unused for now + #'SQLITE_JOURNAL_MODE': {'default': lambda c: 'wal'}, # set at runtime below, interesting if changed later but unused for now because its always expected to be wal #'SQLITE_OPTIONS': {'default': lambda c: ['JSON1']}, # set at runtime below 'USE_CURL': {'default': lambda c: c['USE_CURL'] and (c['SAVE_FAVICON'] or c['SAVE_TITLE'] or c['SAVE_ARCHIVE_DOT_ORG'])}, @@ -465,6 +554,7 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'CHROME_BINARY': {'default': lambda c: c['CHROME_BINARY'] or find_chrome_binary()}, 'USE_CHROME': {'default': lambda c: c['USE_CHROME'] and c['CHROME_BINARY'] and (c['SAVE_PDF'] or c['SAVE_SCREENSHOT'] or c['SAVE_DOM'] or c['SAVE_SINGLEFILE'])}, 'CHROME_VERSION': {'default': lambda c: bin_version(c['CHROME_BINARY']) if c['USE_CHROME'] else None}, + 'CHROME_USER_AGENT': {'default': lambda c: c['CHROME_USER_AGENT'].format(**c)}, 'SAVE_PDF': {'default': lambda c: c['USE_CHROME'] and c['SAVE_PDF']}, 'SAVE_SCREENSHOT': {'default': lambda c: c['USE_CHROME'] and c['SAVE_SCREENSHOT']}, @@ -482,7 +572,7 @@ DYNAMIC_CONFIG_SCHEMA: ConfigDefaultDict = { 'DATA_LOCATIONS': {'default': lambda c: get_data_locations(c)}, 'CHROME_OPTIONS': {'default': lambda c: get_chrome_info(c)}, 'SAVE_ALLOWLIST_PTN': {'default': lambda c: c['SAVE_ALLOWLIST'] and {re.compile(k, ALLOWDENYLIST_REGEX_FLAGS): v for k, v in c['SAVE_ALLOWLIST'].items()}}, - 'SAVE_DENYLIST_PTN': {'default': lambda c: c['SAVE_DENYLIST'] and {re.compile(k, ALLOWDENYLIST_REGEX_FLAGS): v for k, v in c['SAVE_DENYLIST'].items()}}, + 'SAVE_DENYLIST_PTN': {'default': lambda c: c['SAVE_DENYLIST'] and {re.compile(k, ALLOWDENYLIST_REGEX_FLAGS): v for k, v in c['SAVE_DENYLIST'].items()}}, } @@ -498,47 +588,60 @@ def load_config_val(key: str, config_file_vars: Optional[Dict[str, str]]=None) -> ConfigValue: """parse bool, int, and str key=value pairs from env""" + assert isinstance(config, dict) + is_read_only = type is None + if is_read_only: + if callable(default): + return default(config) + return default + + # get value from environment variables or config files config_keys_to_check = (key, *(aliases or ())) + val = None for key in config_keys_to_check: if env_vars: val = env_vars.get(key) if val: break + if config_file_vars: val = config_file_vars.get(key) if val: break - if type is None or val is None: + is_unset = val is None + if is_unset: if callable(default): - assert isinstance(config, dict) return default(config) - return default - elif type is bool: - if val.lower() in ('true', 'yes', '1'): + # calculate value based on expected type + BOOL_TRUEIES = ('true', 'yes', '1') + BOOL_FALSEIES = ('false', 'no', '0') + + if type is bool: + if val.lower() in BOOL_TRUEIES: return True - elif val.lower() in ('false', 'no', '0'): + elif val.lower() in BOOL_FALSEIES: return False else: raise ValueError(f'Invalid configuration option {key}={val} (expected a boolean: True/False)') elif type is str: - if val.lower() in ('true', 'false', 'yes', 'no', '1', '0'): - raise ValueError(f'Invalid configuration option {key}={val} (expected a string)') + if val.lower() in (*BOOL_TRUEIES, *BOOL_FALSEIES): + raise ValueError(f'Invalid configuration option {key}={val} (expected a string, but value looks like a boolean)') return val.strip() elif type is int: - if not val.isdigit(): + if not val.strip().isdigit(): raise ValueError(f'Invalid configuration option {key}={val} (expected an integer)') - return int(val) + return int(val.strip()) elif type is list or type is dict: return json.loads(val) - raise Exception('Config values can only be str, bool, int or json') + raise Exception('Config values can only be str, bool, int, or json') def load_config_file(out_dir: str=None) -> Optional[Dict[str, str]]: @@ -680,9 +783,11 @@ def load_config(defaults: ConfigDefaultDict, return extended_config -# def write_config(config: ConfigDict): -# with open(os.path.join(config['OUTPUT_DIR'], CONFIG_FILENAME), 'w+') as f: +def parse_version_string(version: str) -> Tuple[int, int, int]: + """parses a version tag string formatted like 'vx.x.x' into (major, minor, patch) ints""" + base = version.split('+')[0].split('v')[-1] # remove 'v' prefix and '+editable' suffix + return tuple(int(part) for part in base.split('.'))[:3] # Logging Helpers @@ -771,6 +876,7 @@ def find_chrome_binary() -> Optional[str]: # Precedence: Chromium, Chrome, Beta, Canary, Unstable, Dev # make sure data dir finding precedence order always matches binary finding order default_executable_paths = ( + # '~/Library/Caches/ms-playwright/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium', 'chromium-browser', 'chromium', '/Applications/Chromium.app/Contents/MacOS/Chromium', @@ -1061,9 +1167,9 @@ globals().update(CONFIG) # Set timezone to UTC and umask to OUTPUT_PERMISSIONS -assert TIMEZONE == 'UTC', 'The server timezone should always be set to UTC' # we may allow this to change later -os.environ["TZ"] = TIMEZONE -os.umask(0o777 - int(DIR_OUTPUT_PERMISSIONS, base=8)) # noqa: F821 +assert TIMEZONE == 'UTC', 'The server timezone should always be set to UTC' # noqa: F821 +os.environ["TZ"] = TIMEZONE # noqa: F821 +os.umask(0o777 - int(DIR_OUTPUT_PERMISSIONS, base=8)) # noqa: F821 # add ./node_modules/.bin to $PATH so we can use node scripts in extractors NODE_BIN_PATH = str((Path(CONFIG["OUTPUT_DIR"]).absolute() / 'node_modules' / '.bin')) @@ -1080,7 +1186,6 @@ sys.path.append(NODE_BIN_PATH) # disable stderr "you really shouldnt disable ssl" warnings with library config if not CONFIG['CHECK_SSL_VALIDITY']: import urllib3 - import requests requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -1097,14 +1202,25 @@ if not CONFIG['CHECK_SSL_VALIDITY']: def check_system_config(config: ConfigDict=CONFIG) -> None: ### Check system environment - if config['USER'] == 'root': + if config['USER'] == 'root' or str(config['PUID']) == "0": stderr('[!] ArchiveBox should never be run as root!', color='red') stderr(' For more information, see the security overview documentation:') stderr(' https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview#do-not-run-as-root') + + if config['IN_DOCKER']: + attempted_command = ' '.join(sys.argv[:3]) + stderr('') + stderr(' {lightred}Hint{reset}: When using Docker, you must run commands with {green}docker run{reset} instead of {lightyellow}docker exec{reset}, e.g.:'.format(**config['ANSI'])) + stderr(f' docker compose run archivebox {attempted_command}') + stderr(f' docker run -it -v $PWD/data:/data archivebox/archivebox {attempted_command}') + stderr(' or:') + stderr(f' docker compose exec --user=archivebox archivebox /bin/bash -c "archivebox {attempted_command}"') + stderr(f' docker exec -it --user=archivebox /bin/bash -c "archivebox {attempted_command}"') + raise SystemExit(2) ### Check Python environment - if sys.version_info[:3] < (3, 6, 0): + if sys.version_info[:3] < (3, 7, 0): stderr(f'[X] Python version is not new enough: {config["PYTHON_VERSION"]} (>3.6 is required)', color='red') stderr(' See https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.') raise SystemExit(2) @@ -1180,7 +1296,7 @@ def check_dependencies(config: ConfigDict=CONFIG, show_help: bool=True) -> None: if config['USE_YOUTUBEDL'] and config['MEDIA_TIMEOUT'] < 20: stderr(f'[!] Warning: MEDIA_TIMEOUT is set too low! (currently set to MEDIA_TIMEOUT={config["MEDIA_TIMEOUT"]} seconds)', color='red') - stderr(' Youtube-dl will fail to archive all media if set to less than ~20 seconds.') + stderr(' youtube-dl/yt-dlp will fail to archive any media if set to less than ~20 seconds.') stderr(' (Setting it somewhere over 60 seconds is recommended)') stderr() stderr(' If you want to disable media archiving entirely, set SAVE_MEDIA=False instead:') @@ -1268,8 +1384,7 @@ def setup_django(out_dir: Path=None, check_db=False, config: ConfigDict=CONFIG, with open(settings.ERROR_LOG, "a", encoding='utf-8') as f: command = ' '.join(sys.argv) ts = datetime.now(timezone.utc).strftime('%Y-%m-%d__%H:%M:%S') - f.write(f"\n> {command}; ts={ts} version={config['VERSION']} docker={config['IN_DOCKER']} is_tty={config['IS_TTY']}\n") - + f.write(f"\n> {command}; TS={ts} VERSION={config['VERSION']} IN_DOCKER={config['IN_DOCKER']} IS_TTY={config['IS_TTY']}\n") if check_db: # Enable WAL mode in sqlite3 diff --git a/archivebox/core/admin.py b/archivebox/core/admin.py index 0329d9b0..c4974c3a 100644 --- a/archivebox/core/admin.py +++ b/archivebox/core/admin.py @@ -48,9 +48,11 @@ class TagInline(admin.TabularInline): from django.contrib.admin.helpers import ActionForm from django.contrib.admin.widgets import AutocompleteSelectMultiple +# WIP: broken by Django 3.1.2 -> 4.0 migration class AutocompleteTags: model = Tag search_fields = ['name'] + name = 'tags' class AutocompleteTagsAdminStub: name = 'admin' @@ -60,6 +62,7 @@ class SnapshotActionForm(ActionForm): tags = forms.ModelMultipleChoiceField( queryset=Tag.objects.all(), required=False, + # WIP: broken by Django 3.1.2 -> 4.0 migration widget=AutocompleteSelectMultiple( AutocompleteTags(), AutocompleteTagsAdminStub(), diff --git a/archivebox/core/apps.py b/archivebox/core/apps.py index 5182da05..b1150eb9 100644 --- a/archivebox/core/apps.py +++ b/archivebox/core/apps.py @@ -3,4 +3,5 @@ from django.apps import AppConfig class CoreConfig(AppConfig): name = 'core' + # WIP: broken by Django 3.1.2 -> 4.0 migration default_auto_field = 'django.db.models.UUIDField' diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py index 5b44898b..06e798ab 100644 --- a/archivebox/core/settings.py +++ b/archivebox/core/settings.py @@ -269,6 +269,8 @@ AUTH_PASSWORD_VALIDATORS = [ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] +# WIP: broken by Django 3.1.2 -> 4.0 migration +DEFAULT_AUTO_FIELD = 'django.db.models.UUIDField' ################################################################################ ### Shell Settings diff --git a/archivebox/core/urls.py b/archivebox/core/urls.py index 87261ae2..f89273ff 100644 --- a/archivebox/core/urls.py +++ b/archivebox/core/urls.py @@ -8,6 +8,10 @@ from django.views.generic.base import RedirectView from core.views import HomepageView, SnapshotView, PublicIndexView, AddView, HealthCheckView +# GLOBAL_CONTEXT doesn't work as-is, disabled for now: https://github.com/ArchiveBox/ArchiveBox/discussions/1306 +# from config import VERSION, VERSIONS_AVAILABLE, CAN_UPGRADE +# GLOBAL_CONTEXT = {'VERSION': VERSION, 'VERSIONS_AVAILABLE': VERSIONS_AVAILABLE, 'CAN_UPGRADE': CAN_UPGRADE} + # print('DEBUG', settings.DEBUG) @@ -31,6 +35,9 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), path('admin/', admin.site.urls), + + # do not add extra_context like this as not all admin views (e.g. ModelAdmin.autocomplete_view accept extra kwargs) + # path('admin/', admin.site.urls, {'extra_context': GLOBAL_CONTEXT}), path('health/', HealthCheckView.as_view(), name='healthcheck'), path('error/', lambda _: 1/0), diff --git a/archivebox/core/views.py b/archivebox/core/views.py index 3f3fec12..24035628 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -23,6 +23,7 @@ from ..config import ( PUBLIC_SNAPSHOTS, PUBLIC_ADD_VIEW, VERSION, + COMMIT_HASH, FOOTER_INFO, SNAPSHOTS_PER_PAGE, ) @@ -217,6 +218,7 @@ class PublicIndexView(ListView): return { **super().get_context_data(**kwargs), 'VERSION': VERSION, + 'COMMIT_HASH': COMMIT_HASH, 'FOOTER_INFO': FOOTER_INFO, } diff --git a/archivebox/extractors/__init__.py b/archivebox/extractors/__init__.py index edcf218b..2515b8fd 100644 --- a/archivebox/extractors/__init__.py +++ b/archivebox/extractors/__init__.py @@ -184,7 +184,7 @@ def archive_link(link: Link, overwrite: bool=False, methods: Optional[Iterable[s link.url, command, ts - ) + "\n")) + ) + "\n" + str(e) + "\n")) #f.write(f"\n> {command}; ts={ts} version={config['VERSION']} docker={config['IN_DOCKER']} is_tty={config['IS_TTY']}\n") # print(' ', stats) diff --git a/archivebox/extractors/readability.py b/archivebox/extractors/readability.py index e6e5e061..dc2a06b9 100644 --- a/archivebox/extractors/readability.py +++ b/archivebox/extractors/readability.py @@ -67,13 +67,12 @@ def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEO temp_doc.name, link.url, ] - result = run(cmd, cwd=out_dir, timeout=timeout) try: result_json = json.loads(result.stdout) assert result_json and 'content' in result_json, 'Readability output is not valid JSON' except json.JSONDecodeError: - raise ArchiveError('Readability was not able to archive the page', result.stdout + result.stderr) + raise ArchiveError('Readability was not able to archive the page (invalid JSON)', result.stdout + result.stderr) output_folder.mkdir(exist_ok=True) readability_content = result_json.pop("textContent") @@ -81,8 +80,6 @@ def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEO atomic_write(str(output_folder / "content.txt"), readability_content) atomic_write(str(output_folder / "article.json"), result_json) - # parse out number of files downloaded from last line of stderr: - # "Downloaded: 76 files, 4.0M in 1.6s (2.52 MB/s)" output_tail = [ line.strip() for line in (result.stdout + result.stderr).decode().rsplit('\n', 5)[-5:] @@ -95,11 +92,13 @@ def save_readability(link: Link, out_dir: Optional[str]=None, timeout: int=TIMEO # Check for common failure cases if (result.returncode > 0): - raise ArchiveError('Readability was not able to archive the page', hints) + raise ArchiveError(f'Readability was not able to archive the page (status={result.returncode})', hints) except (Exception, OSError) as err: status = 'failed' output = err - cmd = [cmd[0], './{singlefile,dom}.html'] + + # prefer Chrome dom output to singlefile because singlefile often contains huge url(data:image/...base64) strings that make the html too long to parse with readability + cmd = [cmd[0], './{dom,singlefile}.html'] finally: timer.end() diff --git a/archivebox/extractors/title.py b/archivebox/extractors/title.py index dc496c4e..3505e03f 100644 --- a/archivebox/extractors/title.py +++ b/archivebox/extractors/title.py @@ -66,7 +66,9 @@ def get_html(link: Link, path: Path, timeout: int=TIMEOUT) -> str: """ canonical = link.canonical_outputs() abs_path = path.absolute() - sources = [canonical["singlefile_path"], canonical["wget_path"], canonical["dom_path"]] + + # prefer chrome-generated DOM dump to singlefile as singlefile output often includes HUGE url(data:image/...base64) strings that crash parsers + sources = [canonical["dom_path"], canonical["singlefile_path"], canonical["wget_path"]] document = None for source in sources: try: diff --git a/archivebox/index/sql.py b/archivebox/index/sql.py index 420b9de6..5081c275 100644 --- a/archivebox/index/sql.py +++ b/archivebox/index/sql.py @@ -109,11 +109,13 @@ def write_sql_link_details(link: Link, out_dir: Path=OUTPUT_DIR) -> None: snap = Snapshot.objects.get(url=link.url) except Snapshot.DoesNotExist: snap = write_link_to_sql_index(link) + snap.title = link.title - tag_list = list(dict.fromkeys( - tag.strip() for tag in re.split(TAG_SEPARATOR_PATTERN, link.tags or '') - )) + tag_list = list( + {tag.strip() for tag in re.split(TAG_SEPARATOR_PATTERN, link.tags or '')} + | set(snap.tags.values_list('name', flat=True)) + ) snap.save() snap.save_tags(tag_list) diff --git a/archivebox/logging_util.py b/archivebox/logging_util.py index a52cf82a..3c688a3c 100644 --- a/archivebox/logging_util.py +++ b/archivebox/logging_util.py @@ -393,7 +393,11 @@ def log_link_archiving_finished(link: "Link", link_dir: str, is_new: bool, stats else: _LAST_RUN_STATS.succeeded += 1 - size = get_dir_size(link_dir) + try: + size = get_dir_size(link_dir) + except FileNotFoundError: + size = (0, None, '0') + end_ts = datetime.now(timezone.utc) duration = str(end_ts - start_ts).split('.')[0] print(' {black}{} files ({}) in {}s {reset}'.format(size[2], printable_filesize(size[0]), duration, **ANSI)) @@ -409,7 +413,7 @@ def log_archive_method_finished(result: "ArchiveResult"): """ # Prettify CMD string and make it safe to copy-paste by quoting arguments quoted_cmd = ' '.join( - '"{}"'.format(arg) if ' ' in arg else arg + '"{}"'.format(arg) if (' ' in arg) or (':' in arg) else arg for arg in result.cmd ) @@ -444,12 +448,18 @@ def log_archive_method_finished(result: "ArchiveResult"): for line in list(hints)[:5] if line.strip() ) + docker_hints = () + if IN_DOCKER: + docker_hints = ( + ' docker run -it -v $PWD/data:/data archivebox/archivebox /bin/bash', + ) # Collect and prefix output lines with indentation output_lines = [ *hint_header, *hints, '{}Run to see full output:{}'.format(ANSI['lightred'], ANSI['reset']), + *docker_hints, *([' cd {};'.format(result.pwd)] if result.pwd else []), ' {}'.format(quoted_cmd), ] @@ -517,8 +527,8 @@ def log_shell_welcome_msg(): from .cli import list_subcommands print('{green}# ArchiveBox Imports{reset}'.format(**ANSI)) - print('{green}from core.models import Snapshot, User{reset}'.format(**ANSI)) - print('{green}from archivebox import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) + print('{green}from archivebox.core.models import Snapshot, ArchiveResult, Tag, User{reset}'.format(**ANSI)) + print('{green}from archivebox.cli import *\n {}{reset}'.format("\n ".join(list_subcommands().keys()), **ANSI)) print() print('[i] Welcome to the ArchiveBox Shell!') print(' https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Shell-Usage') diff --git a/archivebox/main.py b/archivebox/main.py index 5ce1e288..76b204b8 100755 --- a/archivebox/main.py +++ b/archivebox/main.py @@ -93,11 +93,16 @@ from .config import ( SQL_INDEX_FILENAME, ALLOWED_IN_OUTPUT_DIR, SEARCH_BACKEND_ENGINE, + LDAP, + get_version, check_dependencies, check_data_folder, write_config_file, VERSION, + VERSIONS_AVAILABLE, + CAN_UPGRADE, COMMIT_HASH, + BUILD_TIME, CODE_LOCATIONS, EXTERNAL_LOCATIONS, DATA_LOCATIONS, @@ -217,32 +222,40 @@ def version(quiet: bool=False, print(VERSION) if not quiet: - # 0.6.3 - # ArchiveBox v0.6.3 Cpython Linux Linux-4.19.121-linuxkit-x86_64-with-glibc2.28 x86_64 (in Docker) (in TTY) - # DEBUG=False IN_DOCKER=True IN_QEMU=False IS_TTY=True TZ=UTC FS_ATOMIC=True FS_REMOTE=False FS_PERMS=644 FS_USER=501:20 SEARCH_BACKEND=ripgrep + # 0.7.1 + # ArchiveBox v0.7.1+editable COMMIT_HASH=951bba5 BUILD_TIME=2023-12-17 16:46:05 1702860365 + # IN_DOCKER=False IN_QEMU=False ARCH=arm64 OS=Darwin PLATFORM=macOS-14.2-arm64-arm-64bit PYTHON=Cpython + # FS_ATOMIC=True FS_REMOTE=False FS_USER=501:20 FS_PERMS=644 + # DEBUG=False IS_TTY=True TZ=UTC SEARCH_BACKEND=ripgrep LDAP=False p = platform.uname() print( - 'ArchiveBox v{}'.format(VERSION), - *((COMMIT_HASH[:7],) if COMMIT_HASH else ()), - sys.implementation.name.title(), - p.system, - platform.platform(), - p.machine, + 'ArchiveBox v{}'.format(get_version(CONFIG)), + *((f'COMMIT_HASH={COMMIT_HASH[:7]}',) if COMMIT_HASH else ()), + f'BUILD_TIME={BUILD_TIME}', + ) + print( + f'IN_DOCKER={IN_DOCKER}', + f'IN_QEMU={IN_QEMU}', + f'ARCH={p.machine}', + f'OS={p.system}', + f'PLATFORM={platform.platform()}', + f'PYTHON={sys.implementation.name.title()}', ) OUTPUT_IS_REMOTE_FS = DATA_LOCATIONS['OUTPUT_DIR']['is_mount'] or DATA_LOCATIONS['ARCHIVE_DIR']['is_mount'] print( - f'DEBUG={DEBUG}', - f'IN_DOCKER={IN_DOCKER}', - f'IN_QEMU={IN_QEMU}', - f'IS_TTY={IS_TTY}', - f'TZ={TIMEZONE}', - #f'DB=django.db.backends.sqlite3 (({CONFIG["SQLITE_JOURNAL_MODE"]})', # add this if we have more useful info to show eventually f'FS_ATOMIC={ENFORCE_ATOMIC_WRITES}', f'FS_REMOTE={OUTPUT_IS_REMOTE_FS}', f'FS_USER={PUID}:{PGID}', f'FS_PERMS={OUTPUT_PERMISSIONS}', + ) + print( + f'DEBUG={DEBUG}', + f'IS_TTY={IS_TTY}', + f'TZ={TIMEZONE}', f'SEARCH_BACKEND={SEARCH_BACKEND_ENGINE}', + f'LDAP={LDAP}', + #f'DB=django.db.backends.sqlite3 (({CONFIG["SQLITE_JOURNAL_MODE"]})', # add this if we have more useful info to show eventually ) print() @@ -271,7 +284,7 @@ def version(quiet: bool=False, print(printable_folder_status(name, path)) else: print() - print('{white}[i] Data locations:{reset}'.format(**ANSI)) + print('{white}[i] Data locations:{reset} (not in a data directory)'.format(**ANSI)) print() check_dependencies() @@ -591,7 +604,7 @@ def add(urls: Union[str, List[str]], out_dir: Path=OUTPUT_DIR) -> List[Link]: """Add a new URL or list of URLs to your archive""" - from core.models import Tag + from core.models import Snapshot, Tag assert depth in (0, 1), 'Depth must be 0 or 1 (depth >1 is not supported yet)' @@ -635,6 +648,19 @@ def add(urls: Union[str, List[str]], write_main_index(links=new_links, out_dir=out_dir) all_links = load_main_index(out_dir=out_dir) + tags = [ + Tag.objects.get_or_create(name=name.strip())[0] + for name in tag.split(',') + if name.strip() + ] + if tags: + for link in imported_links: + snapshot = Snapshot.objects.get(url=link.url) + snapshot.tags.add(*tags) + snapshot.tags_str(nocache=True) + snapshot.save() + # print(f' √ Tagged {len(imported_links)} Snapshots with {len(tags)} tags {tags_str}') + if index_only: # mock archive all the links using the fake index_only extractor method in order to update their state if overwrite: @@ -666,21 +692,8 @@ def add(urls: Union[str, List[str]], stderr(f'[*] [{ts}] Archiving {len(new_links)}/{len(all_links)} URLs from added set...', color='green') archive_links(new_links, overwrite=False, **archive_kwargs) - - # add any tags to imported links - tags = [ - Tag.objects.get_or_create(name=name.strip())[0] - for name in tag.split(',') - if name.strip() - ] - if tags: - for link in imported_links: - snapshot = link.as_snapshot() - snapshot.tags.add(*tags) - snapshot.tags_str(nocache=True) - snapshot.save() - # print(f' √ Tagged {len(imported_links)} Snapshots with {len(tags)} tags {tags_str}') - + if CAN_UPGRADE: + hint(f"There's a new version of ArchiveBox available! Your current version is {VERSION}. You can upgrade to {VERSIONS_AVAILABLE['recommended_version']['tag_name']} ({VERSIONS_AVAILABLE['recommended_version']['html_url']}). For more on how to upgrade: https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives\n") return all_links @@ -1005,9 +1018,9 @@ def setup(out_dir: Path=OUTPUT_DIR) -> None: stderr('\n Installing SINGLEFILE_BINARY, READABILITY_BINARY, MERCURY_BINARY automatically using npm...') if not NODE_VERSION: - stderr('[X] You must first install node using your system package manager', color='red') + stderr('[X] You must first install node & npm using your system package manager', color='red') hint([ - 'curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash -', + 'https://github.com/nodesource/distributions#table-of-contents', 'or to disable all node-based modules run: archivebox config --set USE_NODE=False', ]) raise SystemExit(1) @@ -1155,6 +1168,7 @@ def schedule(add: bool=False, run_all: bool=False, quiet: bool=False, every: Optional[str]=None, + tag: str='', depth: int=0, overwrite: bool=False, update: bool=not ONLY_NEW, @@ -1188,6 +1202,7 @@ def schedule(add: bool=False, 'add', *(['--overwrite'] if overwrite else []), *(['--update'] if update else []), + *([f'--tag={tag}'] if tag else []), f'--depth={depth}', f'"{import_path}"', ] if import_path else ['update']), @@ -1270,6 +1285,9 @@ def schedule(add: bool=False, print('\n{green}[√] Stopped.{reset}'.format(**ANSI)) raise SystemExit(1) + if CAN_UPGRADE: + hint(f"There's a new version of ArchiveBox available! Your current version is {VERSION}. You can upgrade to {VERSIONS_AVAILABLE['recommended_version']['tag_name']} ({VERSIONS_AVAILABLE['recommended_version']['html_url']}). For more on how to upgrade: https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives\n") + @enforce_types def server(runserver_args: Optional[List[str]]=None, diff --git a/archivebox/package.json b/archivebox/package.json index f3d5b736..362cfffe 100644 --- a/archivebox/package.json +++ b/archivebox/package.json @@ -1,13 +1,13 @@ { "name": "archivebox", - "version": "0.7.0", + "version": "0.7.2", "description": "ArchiveBox: The self-hosted internet archive", "author": "Nick Sweeting ", "repository": "github:ArchiveBox/ArchiveBox", "license": "MIT", "dependencies": { "@postlight/parser": "^2.2.3", - "readability-extractor": "git+https://github.com/ArchiveBox/readability-extractor.git", - "single-file-cli": "^1.1.12" + "readability-extractor": "github:ArchiveBox/readability-extractor", + "single-file-cli": "^1.1.46" } } diff --git a/archivebox/static b/archivebox/static new file mode 120000 index 00000000..5d01044d --- /dev/null +++ b/archivebox/static @@ -0,0 +1 @@ +templates/static \ No newline at end of file diff --git a/archivebox/system.py b/archivebox/system.py index 37927ba2..d80a2cb5 100644 --- a/archivebox/system.py +++ b/archivebox/system.py @@ -185,17 +185,19 @@ def dedupe_cron_jobs(cron: CronTab) -> CronTab: class suppress_output(object): - ''' + """ A context manager for doing a "deep suppression" of stdout and stderr in Python, i.e. will suppress all print, even if the print originates in a compiled C/Fortran sub-function. - This will not suppress raised exceptions, since exceptions are printed + + This will not suppress raised exceptions, since exceptions are printed to stderr just before a script exits, and after the context manager has exited (at least, I think that is why it lets exceptions through). with suppress_stdout_stderr(): rogue_function() - ''' + """ + def __init__(self, stdout=True, stderr=True): # Open a pair of null files # Save the actual stdout (1) and stderr (2) file descriptors. diff --git a/archivebox/templates/admin/base.html b/archivebox/templates/admin/base.html index 0592fa0a..5d4d4cc5 100644 --- a/archivebox/templates/admin/base.html +++ b/archivebox/templates/admin/base.html @@ -12,7 +12,26 @@ {% endblock %} - {% block extrastyle %}{% endblock %} + {% block extrastyle %} + + {% endblock %} {% if LANGUAGE_BIDI %} @@ -122,6 +141,43 @@ {% block footer %}{% endblock %} + {% if user.is_authenticated and user.is_superuser and CAN_UPGRADE %} + + {% endif %} +
    -brew install archivebox
    -archivebox version +brew install archivebox
    +archivebox version
    -archivebox init
    +archivebox init
    -archivebox add +archivebox add -archivebox data dir +archivebox data dir
    -archivebox server +archivebox server -archivebox server add +archivebox server add -archivebox server list +archivebox server list -archivebox server detail +archivebox server detail