Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0ae406b0a | ||
|
|
afdeaacbea | ||
|
|
f5124c9e79 | ||
|
|
5fe0d40023 | ||
|
|
3da2a8c091 | ||
|
|
cc3f4e6125 | ||
|
|
5643b5682f | ||
|
|
92a49d1736 | ||
|
|
ce9350cd8e | ||
|
|
04d67887d1 | ||
|
|
afd3df869b | ||
|
|
f6804fe009 | ||
|
|
eda7642af1 | ||
|
|
cab42a44f3 | ||
|
|
89b3c18e9f | ||
|
|
08560ae9ea | ||
|
|
1bcc993a99 | ||
|
|
2054c31575 | ||
|
|
a2278ea1b3 | ||
|
|
8feaae8061 | ||
|
|
a65a5d4b41 | ||
|
|
693b59cc0c | ||
|
|
b85e093d38 | ||
|
|
180cd6f3da | ||
|
|
1de601c505 | ||
|
|
97a481c73d | ||
|
|
4fdb715b54 | ||
|
|
8fbe463adf | ||
|
|
70ee174a47 | ||
|
|
7532f6b569 | ||
|
|
4013e616af | ||
|
|
19daeab0f3 | ||
|
|
4387234e71 | ||
|
|
ad92ec55f8 | ||
|
|
cba62676d0 | ||
|
|
05f50fc546 | ||
|
|
1ee25ac279 | ||
|
|
5ee0ad4a20 | ||
|
|
e52dd8d3f3 | ||
|
|
755afced61 | ||
|
|
0cdba27504 | ||
|
|
3636e3ce4b | ||
|
|
f98d55a14e | ||
|
|
b645314e50 | ||
|
|
95cae0e6c4 | ||
|
|
1fdf04e9c0 |
@@ -1,43 +0,0 @@
|
||||
# `git blame` master ignore list.
|
||||
#
|
||||
# This file contains a list of git hashes of revisions to be ignored
|
||||
# by `git blame`. These revisions are considered "unimportant" in
|
||||
# that they are unlikely to be what you are interested in when blaming.
|
||||
# They are typically expected to be formatting-only changes.
|
||||
#
|
||||
# It can be used for `git blame` using `--ignore-revs-file` or by
|
||||
# setting `blame.ignoreRevsFile` in the `git config`[1].
|
||||
#
|
||||
# Ignore these commits when reporting with blame. Calling
|
||||
#
|
||||
# git blame --ignore-revs-file .git-blame-ignore-revs
|
||||
#
|
||||
# will tell `git blame` to ignore changes made by these revisions when
|
||||
# assigning blame, as if the change never happened.
|
||||
#
|
||||
# You can enable this as a default for your local repository by
|
||||
# running
|
||||
#
|
||||
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
#
|
||||
# This will probably be automatically picked by your IDE
|
||||
# (VSCode+GitLens and JetBrains products are confirmed to do this).
|
||||
#
|
||||
# Important: if you are switching to a branch without this file,
|
||||
# `git blame` will fail with an error.
|
||||
#
|
||||
# GitHub also excludes the commits listed below from its "Blame"
|
||||
# views[2][3].
|
||||
#
|
||||
# [1]: https://git-scm.com/docs/git-blame#Documentation/git-blame.txt-blameignoreRevsFile
|
||||
# [2]: https://github.blog/changelog/2022-03-24-ignore-commits-in-the-blame-view-beta/
|
||||
# [3]: https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
||||
|
||||
|
||||
# Black changes
|
||||
465a88154152fb0607a63fa24c8446bff43ec886
|
||||
f06891926661986fff52d6eb4b4cb120c71972d1
|
||||
9bcbcaefdfecc85aedfd8e2f8aaa1ca7f959404e
|
||||
433dcab02b29f7bd3827e237434034deecc1b549
|
||||
9f6a9f991222efccc87b45a701086c95629c67b6
|
||||
f89114ca7e1b20bf8e645ecd0b52b707ec857aa9
|
||||
31
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Bug report
|
||||
description: >
|
||||
Did you discover a bug in SABnzbd? Report it here!
|
||||
If you are not 100% certain this is a bug please go to our forums, Reddit or Discord server first.
|
||||
labels:
|
||||
- Support
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: SABnzbd version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating system
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Using Docker image
|
||||
options:
|
||||
- linuxserver
|
||||
- hotio
|
||||
- binhex
|
||||
- Other
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Include error logs directly or link to extended logs on https://pastebin.com/
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support forum
|
||||
url: https://forums.sabnzbd.org/
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
- name: Discord
|
||||
url: https://discord.sabnzbd.org
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
- name: Reddit - r/sabnzbd
|
||||
url: https://www.reddit.com/r/sabnzbd
|
||||
about: Support questions can be asked on our forums, Reddit or Discord server.
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,10 +0,0 @@
|
||||
name: Feature request
|
||||
description: What new feature would you like to have added to SABnzbd?
|
||||
labels:
|
||||
- Support
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
1
.github/funding.yml
vendored
@@ -1 +0,0 @@
|
||||
custom: ['https://sabnzbd.org/donate/']
|
||||
47
.github/renovate.json
vendored
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":disableDependencyDashboard"
|
||||
],
|
||||
"schedule": [
|
||||
"before 8am on Monday"
|
||||
],
|
||||
"baseBranches": ["develop", "feature/uvicorn"],
|
||||
"pip_requirements": {
|
||||
"fileMatch": [
|
||||
"requirements.txt",
|
||||
"tests/requirements.txt",
|
||||
"builder/requirements.txt",
|
||||
"builder/release-requirements.txt"
|
||||
]
|
||||
},
|
||||
"ignorePaths": [],
|
||||
"ignoreDeps": [
|
||||
"jaraco.text",
|
||||
"jaraco.context",
|
||||
"jaraco.collections",
|
||||
"sabctools",
|
||||
"paho-mqtt",
|
||||
"werkzeug",
|
||||
"tavern"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchPackageNames": ["windows", "macos"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"*"
|
||||
],
|
||||
"groupName": "all dependencies",
|
||||
"groupSlug": "all",
|
||||
"separateMajorMinor": false,
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"automergeStrategy": "squash",
|
||||
"platformAutomerge": true
|
||||
}
|
||||
256
.github/workflows/build_release.yml
vendored
@@ -1,256 +0,0 @@
|
||||
name: Build binaries and source distribution
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
# Setting PYTHONNODEBUGRANGES reduces binary size
|
||||
env:
|
||||
PYTHONNODEBUGRANGES: 1
|
||||
|
||||
jobs:
|
||||
build_windows:
|
||||
name: Build Windows binary (${{ matrix.architecture }})
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- architecture: x64
|
||||
runs-on: windows-2022
|
||||
- architecture: arm64
|
||||
runs-on: windows-11-arm
|
||||
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Install Python dependencies
|
||||
# Without dependencies to make sure everything is covered in the requirements.txt
|
||||
run: |
|
||||
python --version
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install --upgrade -r requirements.txt --no-dependencies
|
||||
pip install --upgrade -r builder/requirements.txt --no-dependencies
|
||||
- name: Build Windows standalone binary
|
||||
id: windows_binary
|
||||
run: python builder/package.py binary
|
||||
- name: Upload Windows standalone binary (unsigned)
|
||||
uses: actions/upload-artifact@v6
|
||||
id: upload-unsigned-binary
|
||||
with:
|
||||
path: "*-win*-bin.zip"
|
||||
name: Windows standalone binary (${{ matrix.architecture }})
|
||||
- name: Sign Windows standalone binary
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
|
||||
project-slug: "sabnzbd"
|
||||
artifact-configuration-slug: "sabnzbd-binary"
|
||||
signing-policy-slug: "release-signing"
|
||||
github-artifact-id: ${{ steps.upload-unsigned-binary.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows standalone binary (signed)
|
||||
uses: actions/upload-artifact@v6
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: Windows standalone binary (${{ matrix.architecture }}, signed)
|
||||
path: "signed"
|
||||
- name: Build Windows installer
|
||||
if: matrix.architecture == 'x64'
|
||||
run: python builder/package.py installer
|
||||
- name: Upload Windows installer
|
||||
if: matrix.architecture == 'x64'
|
||||
uses: actions/upload-artifact@v6
|
||||
id: upload-unsigned-installer
|
||||
with:
|
||||
path: "*-win-setup.exe"
|
||||
name: Windows installer (${{ matrix.architecture }})
|
||||
- name: Sign Windows installer
|
||||
if: matrix.architecture == 'x64' && contains(github.ref, 'refs/tags/')
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
|
||||
project-slug: "sabnzbd"
|
||||
artifact-configuration-slug: "sabnzbd-installer"
|
||||
signing-policy-slug: "release-signing"
|
||||
github-artifact-id: ${{ steps.upload-unsigned-installer.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "signed"
|
||||
- name: Upload Windows installer (signed)
|
||||
if: matrix.architecture == 'x64' && contains(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Windows installer (${{ matrix.architecture }}, signed)
|
||||
path: "signed/*-win-setup.exe"
|
||||
|
||||
build_macos:
|
||||
name: Build macOS binary
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
|
||||
PYTHON_VERSION: "3.14.2"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
# We need to force compile for universal2 support
|
||||
CFLAGS: -arch x86_64 -arch arm64
|
||||
ARCHFLAGS: -arch x86_64 -arch arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
# Only use this for the caching of pip packages!
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
cache: pip
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Cache Python download
|
||||
id: cache-python-download
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/python.pkg
|
||||
key: cache-macOS-Python-${{ env.PYTHON_VERSION }}
|
||||
- name: Get Python from python.org
|
||||
if: steps.cache-python-download.outputs.cache-hit != 'true'
|
||||
run: curl https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-macos11.pkg -o ~/python.pkg
|
||||
- name: Install Python
|
||||
run: sudo installer -pkg ~/python.pkg -target /
|
||||
- name: Install Python dependencies
|
||||
# We have to manually compile some modules as they don't automatically fetch universal2 binaries
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 install --upgrade pip wheel
|
||||
pip3 install --upgrade -r requirements.txt --no-binary cffi,CT3,PyYAML,charset_normalizer --no-dependencies
|
||||
pip3 install --upgrade -r builder/requirements.txt --no-dependencies
|
||||
- name: Import macOS codesign certificates
|
||||
# Taken from https://github.com/Apple-Actions/import-codesign-certs/pull/27 (comments)
|
||||
env:
|
||||
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
||||
CERTIFICATES_P12_PASSWORD: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
MACOS_KEYCHAIN_TEMP_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_TEMP_PASSWORD }}
|
||||
if: env.CERTIFICATES_P12
|
||||
run: |
|
||||
echo $CERTIFICATES_P12 | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
|
||||
security set-keychain-settings -lut 21600 build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$CERTIFICATES_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign -T /usr/bin/xcrun
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_TEMP_PASSWORD" build.keychain
|
||||
- name: Build source distribution
|
||||
# Run this on macOS so the line endings are correct by default
|
||||
run: python builder/package.py source
|
||||
- name: Upload source distribution
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: "*-src.tar.gz"
|
||||
name: Source distribution
|
||||
- name: Build macOS binary
|
||||
env:
|
||||
SIGNING_AUTH: ${{ secrets.SIGNING_AUTH }}
|
||||
NOTARIZATION_USER: ${{ secrets.NOTARIZATION_USER }}
|
||||
NOTARIZATION_PASS: ${{ secrets.NOTARIZATION_PASS }}
|
||||
run: |
|
||||
python3 builder/package.py app
|
||||
python3 builder/make_dmg.py
|
||||
- name: Upload macOS binary
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: "*-macos.dmg"
|
||||
name: macOS binary
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Packages (${{ matrix.linux_arch }})
|
||||
timeout-minutes: 30
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
linux_arch: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
linux_arch: arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Cache par2cmdline-turbo tarball
|
||||
uses: actions/cache@v5
|
||||
id: cache-par2cmdline
|
||||
# Clearing the cache in case of new version requires manual clearing in GitHub!
|
||||
with:
|
||||
path: snap/par2cmdline.tar.gz
|
||||
key: cache-par2cmdline
|
||||
- name: Download par2cmdline-turbo tarball
|
||||
if: steps.cache-par2cmdline.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
PAR2_TARBALL=$(curl -sL https://api.github.com/repos/animetosho/par2cmdline-turbo/releases/latest | jq -r '.tarball_url')
|
||||
curl -o snap/par2cmdline.tar.gz -L "$PAR2_TARBALL"
|
||||
- uses: snapcore/action-build@v1
|
||||
name: Build snap
|
||||
id: snapcraft
|
||||
- name: Test snap installation
|
||||
run: |
|
||||
sudo snap install --dangerous *.snap
|
||||
sudo snap connect sabnzbd:removable-media
|
||||
# Basic smoke test - check that the binary exists and can show help
|
||||
timeout 10s snap run sabnzbd --help || true
|
||||
sudo snap remove sabnzbd
|
||||
- name: Upload snap
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Snap package (${{ matrix.linux_arch }})
|
||||
path: ${{ steps.snapcraft.outputs.snap }}
|
||||
- name: Publish snap
|
||||
uses: snapcore/action-publish@v1
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }}
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_TOKEN }}
|
||||
snap: ${{ steps.snapcraft.outputs.snap }}
|
||||
release: stable
|
||||
|
||||
release:
|
||||
name: Prepare Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_windows, build_macos]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
cache: pip
|
||||
cache-dependency-path: "builder/release-requirements.txt"
|
||||
- name: Download Source distribution artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: Source distribution
|
||||
- name: Download macOS artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macOS binary
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: ${{ (contains(github.ref, 'refs/tags/')) && '*signed*' || '*Windows*' }}
|
||||
merge-multiple: true
|
||||
- name: Prepare official release
|
||||
env:
|
||||
AUTOMATION_GITHUB_TOKEN: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
|
||||
REDDIT_TOKEN: ${{ secrets.REDDIT_TOKEN }}
|
||||
run: |
|
||||
pip3 install -r builder/release-requirements.txt
|
||||
python3 builder/release.py
|
||||
63
.github/workflows/integration_testing.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: CI Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
black:
|
||||
name: Black Code Formatter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Black Code Formatter
|
||||
uses: lgeiger/black-action@master
|
||||
with:
|
||||
args: >
|
||||
SABnzbd.py
|
||||
sabnzbd
|
||||
scripts
|
||||
tools
|
||||
builder
|
||||
builder/SABnzbd.spec
|
||||
tests
|
||||
--line-length=120
|
||||
--target-version=py39
|
||||
--check
|
||||
--diff
|
||||
|
||||
test:
|
||||
name: Test ${{ matrix.name }} - Python ${{ matrix.python-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ]
|
||||
name: ["Linux"]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- name: macOS
|
||||
os: macos-latest
|
||||
python-version: "3.14"
|
||||
- name: Windows
|
||||
os: windows-2022
|
||||
python-version: "3.14"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: "**/requirements.txt"
|
||||
- name: Install system dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get install unrar 7zip par2
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python --version
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install --upgrade -r requirements.txt --no-dependencies
|
||||
pip install --upgrade -r tests/requirements.txt
|
||||
- name: Test SABnzbd
|
||||
run: pytest -s
|
||||
33
.github/workflows/stale.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: "Close and lock old issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: "Close stale issues"
|
||||
if: github.repository_owner == 'sabnzbd'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 21
|
||||
days-before-close: 7
|
||||
stale-issue-label: "Stale"
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
exempt-issue-labels: "Feature request, Work in progress, Bug"
|
||||
|
||||
lock:
|
||||
name: "Lock old issues"
|
||||
if: github.repository_owner == 'sabnzbd'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v6
|
||||
with:
|
||||
log-output: true
|
||||
issue-inactive-days: 60
|
||||
pr-inactive-days: 60
|
||||
43
.github/workflows/translations.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Update translatable texts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
translations:
|
||||
name: Update translatable texts
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TX_TOKEN: ${{ secrets.TX_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
|
||||
- name: Generate translatable texts
|
||||
run: |
|
||||
python3 tools/extract_pot.py
|
||||
- name: Push/pull Transifex translations
|
||||
if: env.TX_TOKEN
|
||||
# Add --translation to the push command in order to update Transifex using local translation edits
|
||||
# However, this prevents modifying existing translations in Transifex as they will be overwritten by the push!
|
||||
run: |
|
||||
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash
|
||||
./tx push --source
|
||||
./tx pull --all --force
|
||||
- name: Compile translations to validate them
|
||||
run: |
|
||||
python3 tools/make_mo.py
|
||||
- name: Push translatable and translated texts back to repo
|
||||
uses: stefanzweifel/git-auto-commit-action@v7.1.0
|
||||
if: env.TX_TOKEN
|
||||
with:
|
||||
commit_message: |
|
||||
Update translatable texts
|
||||
[skip ci]
|
||||
commit_user_name: SABnzbd Automation
|
||||
commit_user_email: bugs@sabnzbd.org
|
||||
commit_author: SABnzbd Automation <bugs@sabnzbd.org>
|
||||
file_pattern: "po/*.pot po/*.po"
|
||||
push_options: --force
|
||||
47
.gitignore
vendored
@@ -1,53 +1,28 @@
|
||||
# Compiled python and venv
|
||||
*.py[cod]
|
||||
__pycache__
|
||||
/venv
|
||||
#Compiled python
|
||||
*.py[co]
|
||||
|
||||
# Snapcraft
|
||||
parts/
|
||||
prime/
|
||||
stage/
|
||||
snap/.snapcraft/
|
||||
*.snap
|
||||
|
||||
# Generated email templates
|
||||
email/*.tmpl
|
||||
|
||||
# Working folders for build
|
||||
# Working folders for Win build
|
||||
build/
|
||||
dist/
|
||||
locale/
|
||||
srcdist/
|
||||
|
||||
# Generated email templates
|
||||
email/
|
||||
|
||||
# Romanian ro.po is generated from ro.px, due to mapping to latin-1
|
||||
po/*/ro.po
|
||||
|
||||
# Build results
|
||||
SABnzbd*.zip
|
||||
SABnzbd*.exe
|
||||
SABnzbd*.gz
|
||||
SABnzbd*.dmg
|
||||
SABnzbd*.pkg
|
||||
SABnzbd-*/
|
||||
|
||||
# WingIDE/PyCharm project files
|
||||
*.wp[ru]
|
||||
.idea
|
||||
|
||||
# VScode
|
||||
.vscode/
|
||||
|
||||
# Testing folders
|
||||
.cache
|
||||
.xprocess
|
||||
.pytest*
|
||||
tests/cache
|
||||
# WingIDE project file
|
||||
*.wpr
|
||||
|
||||
# General junk
|
||||
*.keep
|
||||
*.bak
|
||||
*.log
|
||||
|
||||
# Some people use Emacs as an editor
|
||||
\#*
|
||||
.\#*
|
||||
|
||||
# Apple
|
||||
.DS_Store
|
||||
|
||||
23
.tx/config
@@ -1,23 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:sabnzbd:p:sabnzbd-translations:r:po-main-sabnzbd-pot--develop]
|
||||
file_filter = po/main/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/main/SABnzbd.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:sabnzbd:p:sabnzbd-translations:r:po-email-sabemail-pot--develop]
|
||||
file_filter = po/email/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/email/SABemail.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:sabnzbd:p:sabnzbd-translations:r:po-nsis-sabnsis-pot--develop]
|
||||
file_filter = po/nsis/<lang>.po
|
||||
minimum_perc = 0
|
||||
source_file = po/nsis/SABnsis.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
63
ABOUT.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
*******************************************
|
||||
*** This is SABnzbd 0.6.12 ***
|
||||
*******************************************
|
||||
SABnzbd is an open-source cross-platform binary newsreader.
|
||||
It simplifies the process of downloading from Usenet dramatically,
|
||||
thanks to its friendly web-based user interface and advanced
|
||||
built-in post-processing options that automatically verify, repair,
|
||||
extract and clean up posts downloaded from Usenet.
|
||||
SABnzbd also has a fully customizable user interface,
|
||||
and offers a complete API for third-party applications to hook into.
|
||||
|
||||
There is an extensive Wiki on the use of SABnzbd.
|
||||
http://wiki.sabnzbd.org/
|
||||
|
||||
IMPORTANT INFORMATION about release 0.6.0:
|
||||
http://wiki.sabnzbd.org/introducing-0-6-0
|
||||
|
||||
|
||||
Please also read the file "ISSUES.txt"
|
||||
|
||||
*******************************************
|
||||
*** Upgrading from 0.6.x ***
|
||||
*******************************************
|
||||
Stop SABnzbd.
|
||||
Install new version
|
||||
Start SABnzbd.
|
||||
|
||||
|
||||
*******************************************
|
||||
*** Upgrading from 0.5.x ***
|
||||
*******************************************
|
||||
Stop SABnzbd.
|
||||
Uninstall current version, keeping the data.
|
||||
Install new version
|
||||
Start SABnzbd.
|
||||
|
||||
The organization of the download queue is different from 0.5.x.
|
||||
0.6.x will finish downloading an existing queue, but you
|
||||
cannot go back to an older version without losing your queue.
|
||||
Also, your sabnzbd.ini file will be upgraded, making it
|
||||
incompatible with release 0.5.x
|
||||
|
||||
|
||||
*******************************************
|
||||
*** Upgrading from 0.4.x ***
|
||||
*******************************************
|
||||
|
||||
>>>>> PLEASE DOWNLOAD YOUR CURRENT QUEUE BEFORE UPGRADING <<<<<<
|
||||
|
||||
When upgrading from a 0.4.x release such as 0.4.12 your old settings will be kept.
|
||||
You will however be given a fresh queue and history. If you have items in your queue
|
||||
from the older version of SABnzbd, you can either re-import the nzb files if you kept
|
||||
an nzb backup folder, or temporarily go back to 0.4.x until your queue is complete.
|
||||
|
||||
The history is now stored in a better format meaning future upgrades should be backwards
|
||||
compatible.
|
||||
|
||||
|
||||
*******************************************
|
||||
*** Changes since 0.5.6 ***
|
||||
*******************************************
|
||||
|
||||
See: http://wiki.sabnzbd.org/introducing-0-6-0
|
||||
871
CHANGELOG.txt
Normal file
@@ -0,0 +1,871 @@
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.12Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix issue with new localhost handling on some IPv4-only Unixes
|
||||
- Fix job folder creation by Movie Sort when the Sort expression specifies one
|
||||
- Fix problem with retrieving ZIP files from some web sites
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.11Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Better support for Yahoo pipes
|
||||
- Accept NZB files containing incorrect dates
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.11RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix some Growl issues (OSX)
|
||||
- Improve detection of encrypted RAR files during download
|
||||
- SABnzbd will now listen on all "localhost" addresses
|
||||
- Remove unneeded extra temporary folder level in Generic Sort
|
||||
- Show the promised 10 queue entries in the OSX menu instead of 9
|
||||
- Fix logging of pre-queue script result
|
||||
- "Get bookmarks now" for newzbin only worked when automatic readout was also enabled.
|
||||
- When par2 fails do an SFV-based check when SFV files present and feature enabled.
|
||||
(previously SFV was only used when no par2 files were available at all)
|
||||
- Do extra checks on job administration on disk
|
||||
(an attempt to diagnose jobs with missing admin files)
|
||||
- Make newzbin "Get bookmarks now" button independent of automatic readout
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.10Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Convert ambiguous Windows paths like D: and D:folder to D:\ and D:\folder
|
||||
- Fix file name encoding problems when verifying using SFV files
|
||||
- Add GNTP module to source distribution
|
||||
- Prevent reading newzbin bookmarks when newzbin credentials are not set
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.10RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Allow saving of category paths ending in a *
|
||||
This is will prevent the creation of job folders in the final folder
|
||||
- Fix incompatibility with unrar 4.01 regarding detection of encrypted files
|
||||
- Create .bak (backup) file for sabnzbd.ini before modifying it
|
||||
- OSX: Compatible with Growl 1.2.2 and 1.3
|
||||
- OSX: Prevent changes to SABnzbd.app folder which confused the OSX Firewall
|
||||
- OSX: Fix access rights of SABnzbs.app so that restricted users can run SABnzbd
|
||||
- OSX: Combined SnowLeopard/Lion DMG and separate Leopard DMG
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.9Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Change the OSX DMG window size (again) so that the optional "path bar"
|
||||
doesn't obsure part of the background image.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.9RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Generic Sorter failed to uppercase first letter of title when starting with "the/a/to" etc.
|
||||
- Add "hidden" option allow_64bit_tools (lost when going from 0.5.6 to 0.6.0)
|
||||
- Improve handling of non-creatable final and category folders
|
||||
- Fix and I18N the "Downloaded in X days Y hours Z seconds" text in the history report.
|
||||
- Change the OSX DMG window size so that the optional "path bar" doesn't obsure part of the background image.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.9RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Update Plush to solve minor browser incompatibilities
|
||||
- On Windows the 64bit versions of par2 and unrar were never used
|
||||
- Updated unrar to 4.01
|
||||
- Using the "Download" button in newzbin.com RSS feeds produced malformed names.
|
||||
- For OSX make a Lion and a Leopard/SnowLeopard version and show this in DMG background image
|
||||
- When removing job folders in the "temporary download folder", remove everything.
|
||||
This is needed because some operating systems add spurious files and folders.
|
||||
- Servers with square brackets in the name could not be removed.
|
||||
- Check email parameters also when only email_rss is set.
|
||||
- Try out work-around for potential memory leak in pickle.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.8Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix several RSS issues, due to corruption of stored RSS data
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.7Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Prevent immediate exit after startup when queue is empty and
|
||||
an end-of-queue action like "shutdown" was set
|
||||
- Fix failure to launch browser when clicking shortcut the second time
|
||||
- Allow jobs still waiting for post-processing to be deleted
|
||||
- Delay starting of dowload task until after webserver is started.
|
||||
- Plush: button "Purge failed NZBs & delete files" will now actually delete files
|
||||
- Classic/smpl: add link "Purge failed NZBs & delete files"
|
||||
- Plush: fix flashing top menu in Safari 5.1
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.6Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Prevent crashes when running into disk-full situations.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.6RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- When "Download only" is used, do not send downloaded NZB files to the queue
|
||||
- Fix bad links coming from nzbclub.com
|
||||
- A job sometimes fails verification when the option "don't download samples" is used.
|
||||
Now this option will be ignored when you click "Retry" in the history.
|
||||
- File an error message when the RSS-email template is missing.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.6Beta2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix sending of duplicate emails when using a list of recipients
|
||||
- Fix handle leakage on Windows
|
||||
- On OSX, SABnzbd didn't handle "Open With" of nzb.gz files properly
|
||||
- Limit the amount of retries when getting a partial NZB from an index site
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.6Beta1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Compatible with OSX Lion
|
||||
- End-of-queue action now ignores paused items in the queue
|
||||
- Fetching extra par2 files now obeys pause too
|
||||
- Extension-based cleanup now also cleans sub-folders
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.5Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Since 0.6.3, all end-of-queue actions failed.
|
||||
- Generic sort: failed to recognize years surounded by underscores.
|
||||
- When running the Wizard in German, the last page crashed.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.4Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix silly error in the Wizard
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.3Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Save bookmark memory file right after reading newzbin's bookmarks
|
||||
- Add new command line option --new to force new SABnzbd instance
|
||||
- Plush: add "missing articles" to queue entry hover-over popup
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.3RC3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Regression error: paused duplicates did not end in the queue
|
||||
- Better support for standby and hibernate on Linux
|
||||
- Remove the delay when items go from the download queue to History
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.3RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- On OSX, the top menu sometimes uses the wrong URL to start the browser
|
||||
- API: jsonp-callback now also supported in error responses
|
||||
- When a job is individually pause, save admin immediately
|
||||
- Sorting: support %fn parameter in folder names
|
||||
- Allow a server definition with a non-resolving name to be disabled.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.3RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Move option "Check unpack results" to Config->Switches
|
||||
You may want to set this on because the default was off.
|
||||
- Automatic redirection when addressing the HTTPS port using the HTTP url.
|
||||
- Option to store the files of all jobs in a single category folder
|
||||
(enable this by appending a '*' to the storage path)
|
||||
- The smpl skin failed to save the attributes of the default category
|
||||
- Replace double quote characters in job folder names (to avoid Samba issues)
|
||||
- When using the download button in an RSS overview, the title wasn't used as job name
|
||||
- Another attempt to fix the NZB-association problem on Windows 7 with HTTPS
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.2Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed association of NZB files with SABnzbd, when using HTTPS-only on Windows Vista/7
|
||||
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
|
||||
- Fixed incorrect email date header when using other language than English
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.1Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed association of NZB files with SABnzbd (for real this time).
|
||||
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.1RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed association of NZB files with SABnzbd
|
||||
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
|
||||
- Fixed display of warning when upgrading from 0.4.x
|
||||
- Fixed problem when entering NZB files with names containing accented characters
|
||||
(happened only on OSX with Firefox 4)
|
||||
- Fixed several textual issues
|
||||
- Add PID file support for OSX/Unix (--pid <path>)
|
||||
- Fix attribute error caused by missing category field in nzbmatrix RSS feeds
|
||||
- Fix nzbmatrix category support by just reading the whole "Description" field.
|
||||
- Pause downloader when downloaded file cannot be written to disk.
|
||||
- Fix "~/" (home-dir) support for Unix/OSX and disable for Windows
|
||||
- Add Romanian translation
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix handling of semicolon in parameters for user scripts.
|
||||
- Fix attribute handling of 'addlocalfile' API.
|
||||
- Fixed broken Mobile skin
|
||||
- Prevent loss of sabnzbd.ini when disk is full
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0RC4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed History in Classic skin
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0RC3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed History paging with regards to queued jobs
|
||||
- Changing category in the queue failed to change to the PP setting.
|
||||
- Add support for categories in RSS feeds from nzb.su
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Category returned by pre-queue script wasn't handled properly
|
||||
- Stop warning about duplicate articles in an NZB, just summarize after download
|
||||
- Speed-up the generation of RSS filter/preview page
|
||||
- "paused" priority at the RSS feed-level wasn't handled properly
|
||||
- Plush: fix some display issues
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix hangup during scheduled restart.
|
||||
- OSX: Launch browser from menu bar with HTTPS when only HTTPS is available.
|
||||
- Fix problem that caused SABnzbd always to startup in paused state.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Beta5 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Plush: added TopMenu-toggle and show warning-line only when needed
|
||||
- Plush: added "Now" menu entries for bookmarks, RSS and watched folder
|
||||
- Fix failing scheduled event "pause-postprocessing"
|
||||
- Improve handling of nzbmatrix waiting times
|
||||
- Fix sub-optimal handling of password tries on encrypted rar files
|
||||
- Allow "Default" category to be picked for all Sort functions
|
||||
- Fix server test function
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Beta4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Unix/OSX: fixed download hangup when having non-responding servers
|
||||
- Fixed paging problem in Plush
|
||||
- Plush now uses full window width
|
||||
- New server could be not added in "disabled" mode
|
||||
- Prevent a discarded duplicate job from showing up as an orphan
|
||||
- Fixed disabled default script in Plush
|
||||
- Emails to multiple recipients are now sent per recipient
|
||||
allowing more flexibility in email templates.
|
||||
- Moved "email test" button to Config->Email
|
||||
- Plush cookies renamed to prevent interference by older versions
|
||||
- Stop "growl"-ing all queued jobs at startup (OSX)
|
||||
- Unix/OSX: prevent queue lockup when server connect times out
|
||||
- Fix problem with setting a scheduled server action
|
||||
- The API should give the same byte totals as the UI
|
||||
- Move email test to Config->Email
|
||||
- Fix loss of PP settings when retrying
|
||||
- When re-evaluating RSS entries, use original RSS categories
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Beta3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed odd post-processing after upgrade
|
||||
- Fixed "Send Group" option
|
||||
- Fixed incorrect error handling for nzbmatrix unbookmarking
|
||||
- Remove "unknown decoding" message for files without any downloaded articles
|
||||
- Unstarted jobs inherited after upgrade would not download properly
|
||||
- Fix failing server test after change of server address
|
||||
- Add "Read all feeds now" button (will read and process all enabled feeds)
|
||||
- Add 'nzb_key' for "add nzb"-only API access
|
||||
- Prevent crash when incorrect host address triggers unexpected error code
|
||||
- Interpret 0-sized NZB files from nzbmatrix as "not found" (and don't retry forever)
|
||||
- Three way option for duplicate detection (off/ignore/pause)
|
||||
- Plush: lots of styling, navigation and browser compatibility fixes
|
||||
- Windows installer will now add a "Safe" entry to the Start menu
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Beta2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix problem in smpl skin's RSS download buttons
|
||||
- Fix paging and flag display in Wizard
|
||||
- Add clear-downloaded jobs in RSS
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Beta1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Text and translation fixes
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha13 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Duplicate NZBs will not be rejected but entered the queue
|
||||
as "paused" and flagged. Manual resuming will start the download anyway.
|
||||
- Add INI-only option to ignore unrar timestamps
|
||||
- More Plush touch-ups
|
||||
- Translation updates
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha12 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Delete from History did not always work
|
||||
- "Add server" dialog was broken
|
||||
- Add confirmation dialog to deleting from RSS feed table
|
||||
- Improve Queue and History deletion and purge in Plush
|
||||
- Cosmetic improvements to Plush
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha11 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Plush tweaks
|
||||
- Failed URL-based NZB fetches were not moved to History but became dead queue entries
|
||||
- Empty NZB files are now refused
|
||||
- Make some hard-coded texts in Plush translatable
|
||||
- Encrypted rar files without available password were not reported properly
|
||||
- Remove race condition which could cause crash of URL-based NZB fetches
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha10 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed issue in email that prevented HTML emails templates from working
|
||||
- Identical host:port combos are allowed for Usenet servers now
|
||||
- After auto-pause om encrypted files don't stop again after the user resumes
|
||||
- In RSS feeds give the feed-level category priority over the trackers's derived category
|
||||
- Add API handlers for new [rss] and [categories] entries.
|
||||
- Fix incorrect handling of End-Of-Queue script when NICE or IONICE parameters have been set
|
||||
- Filter hidden files and folders from script pick list
|
||||
- Make "folder_check" accessible from the UI
|
||||
- End-of-queue action could be be cleared properly from the INI file
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha9 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Plush top menu and navigation improvements
|
||||
- Active jobs sometimes showed up as orphaned jobs
|
||||
- Post processing retries in SMPL skin (not fully funtional)
|
||||
- Apply category attributes to RSS feed jobs correctly
|
||||
- Drop support for Python 2.4
|
||||
- Add json-p support by support 'callback' parameter in API
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha8 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Prevent active and waiting post-processing jobs from showing up
|
||||
in the orphans list.
|
||||
- Simplified Wizard (No skin selection, Usenet server first)
|
||||
- Accept non-alphanumerical characters in {{category}}name.nzb
|
||||
- Add priority support to RSS feeds
|
||||
- Add 'paused' as an extra priority in RSS feeds
|
||||
- Add %dn to Series terms (original job name)
|
||||
- Add hidden option "never_repair".
|
||||
- The Windows version writes its API URL to the registry
|
||||
- Colons were removed from folder names
|
||||
- Fix crash when an RSS feed had no filters
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha7 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Post-processing would crash on empty category folder
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha6 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Features:
|
||||
- Plush now has tabbed Config pages
|
||||
- Add retention time parameter to servers so that articles too old will not be tried
|
||||
- Disable top_only option (interferes with "retention" implementation)
|
||||
- Option to automatically remove nzbmatrix bookmark after successful download
|
||||
Bugs:
|
||||
- Prevent blocking of RSS preview by automatic RSS read-out
|
||||
- Windows binary had issues with sending non-english emails
|
||||
- Explicit RSS filter attributes (category, script, pp) influenced each other
|
||||
within a feed definition
|
||||
- rss_scan is now a scheduable event and will override the rss_rate option.
|
||||
- Improve handling of colon characters in filenames (compensating for par2 issues)
|
||||
- NZBs would be left behind, despite ".nzb" being in the cleanup-list
|
||||
- When only individually paused jobs are in the queue, unpausing them
|
||||
would not always resume downloading
|
||||
- Use nzbmatrix.com's new V1.1 api.
|
||||
- Sanitize nzbname parameter coming from API calls (led to problems with some
|
||||
third-party utilities)
|
||||
- NZB-chaining should ignore the duplicate NZB check and fix cleanup
|
||||
of "complete" folder after NZB-chaining.
|
||||
- Plush did not save end-of-queue action.
|
||||
- Zipped NZB files from URLs were not accepted.
|
||||
- OSX: The end-of-queue action "Hibernate" shut down the system.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha5 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Plush now has multi operations for the queue
|
||||
- You can now see any orphaned folders in "temporary download folder"
|
||||
and choose to delete or to re-add to the queue (Classic: Config-Main, others: Connections)
|
||||
(This replaces the "Scan" button, which was a blunt instrument)
|
||||
- There is now Default category, which provides attributes for all "Default" settings.
|
||||
The default PP, script and priority in Config->Switches are therefor removed.
|
||||
- nzbmatrix bookmarks are removed after successful download/post-processing
|
||||
(If you really don't want that, use the INI-only 'del_bookmark' option)
|
||||
- NZB chaining now ignores duplicate check.
|
||||
(Chaining is automatic re-queuing of a download containing only NZB(s))
|
||||
- Improve sorting of rar files to enable streaming
|
||||
- Normally when enabling HTTPS, the HTTP port will still be active.
|
||||
Now you can choose to run HTTPS-only on the standard port.
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix crash after queue repair cycle, which prevented restart
|
||||
- end-of-queue hibernate on OSX did shutdown instead of sleep
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Season sort got confused when show title contained a dot
|
||||
- Restore newzbin support in Plush
|
||||
- Show speeds from MB and higher with one decimal
|
||||
- Restore detection of empty downloads
|
||||
- Fix crash in post-processor caused by non-plain-ASCII in user script output
|
||||
- Proper converion of 0.5.x language codes and skin names
|
||||
- A repair scan would pick up folders waiting for retry in History, creating double entries
|
||||
- Small fixes
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Proper packaging
|
||||
-------------------------------------------------------------------------------
|
||||
0.6.0Alpha1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Redesign of the download queue allowing repairs and retries of post-processing
|
||||
- pre-queue user script
|
||||
- optional email sent when RSS sends jobs to the queue
|
||||
- Redesign of the Config-RSS pages (note: "smpl" does not work here)
|
||||
- Full Windows Service support
|
||||
- Byte counters per server and independent of History database
|
||||
- Many bugfixes
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.5RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Add Norwegian translation
|
||||
- Remove illegal characters from nzbname parameter in API calls
|
||||
- Add INI-only option "api_warnings" to enable/disable warnings
|
||||
about unauthenticated access to the API.
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.4Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Ensure that sabnzbd.ini has no group/world access (Unix, OSX)
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.4RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Support unpacking of rar-files embedded in zip-files
|
||||
- Compensate for IPV6 issues of Chrome on OSX
|
||||
- Changes in dirscan-speed are now immediately effective
|
||||
- Reduce amount of logging
|
||||
- On Windows, detect whether sqlite3.dll is missing. If so, panic and
|
||||
complain about lame virus scanner.
|
||||
- Add hidden option "queue_complete_pers" to select non-persistence of
|
||||
non-script end-of-queue actions.
|
||||
- Restrict square bracket substitution to server names
|
||||
- Add hidden option "allow_64bit_tools" to allow or prevent use of 64bit par2 and unrar.
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.3Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Embed nzbmatrix categories in SABnzbd (preventing problem in OSX)
|
||||
- Remove leading spaces in user script output display
|
||||
- Solve problems with passing filenames with '&' as parameter in user scripts
|
||||
- Allow leading empty lines in email templates
|
||||
- Prevent oversized logfile when logging fails
|
||||
- Remove double header in email (some email server choke on that)
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.3RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed error in 101 season/episode handling (prevent years like 2010 from matching)
|
||||
- Fixed error in saving some email options
|
||||
- Accept ".nzb.gz" files coming from an URL fetch.
|
||||
- Make sysload support for Linux more universal
|
||||
- Restore newzbin support in UI
|
||||
- RSS feeds are now listed in alphabetic order
|
||||
- Fixed error handling & in names sent to post-processing script (Windows)
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.3RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Support of nzbmatrix categories
|
||||
- Support of X-DNZB headers for all indexing sites (allows categories for nzbs.org)
|
||||
- Support 101 season/episode notation
|
||||
- Use all text after the season/episode element as episode name (don't require '-')
|
||||
- Support of categories in RSS feeds
|
||||
(only for compatible feeds, like nzbmatrix.com and nzbs.org)
|
||||
- Accumulate all groups from an NZB (for category matching)
|
||||
- After reducing connections SABnzbd should retry full amount 10 min later
|
||||
This event was sometimes missed
|
||||
- Recursive unpack of archives should be done in nested folders instead of base folder
|
||||
- Sort strings with an absolute path were not handled properly
|
||||
- Restore compatibility with Python 2.4
|
||||
- Several fixes to improve robustness of NZB retrieval from indexers
|
||||
- Solved crash in NZB retrieval from indexers when explicit PP value was set
|
||||
- Remove newzbin features from UI
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.2 Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- API: &nzbname= was ignored for some sites
|
||||
- API: Server rename did not work properly
|
||||
- Remember end-of-queue action for next end-of-queue
|
||||
- Some updates to Danish
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.2 RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fixed: Large queue caused very slow UI and high memory usage
|
||||
- Fixed: Some very large rar sets failed to unpack
|
||||
- Fixed: garbled emails were sent
|
||||
- Python user scripts now run properly on OSX
|
||||
- Fixed: SeasonSort could produce trailing spaces, which fail on Windows
|
||||
- Fixed: unpacking errors not properly reported to user script
|
||||
- Keep trying when receiving garbage from nzbmatrix.com
|
||||
- Add total-size fields to API-call History.
|
||||
- Auto-search for free port when specified port is occupied
|
||||
- Improve reporting of newzbin errors
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.1 Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Fix accented character problems in emails
|
||||
- Warn when user doesn't have permission to use a web port (Linux/OSX)
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.1 RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Accept comma-separated email address in "email_to" option.
|
||||
- Allow manual retry of URL-based NZB fetches when a bad NZB is received
|
||||
- Add INI-file option "size_limit" that sets any oversized job
|
||||
to paused and low priority.
|
||||
- Added French translation of email template
|
||||
- Add Danish translation
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.1 RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Add 'size_limit' option. Any job bigger than this limit will be set to
|
||||
low priority and be paused.
|
||||
- TV Season sort now has "affected categories" too. Check your settings!!
|
||||
- Fixed problems that could lead to an infinite unpacking loop
|
||||
(when using a "sick" NZB and using +U instead of +D postprocessing)
|
||||
- Duplicate title checking in RSS is now done across all feeds
|
||||
- Fixed issues with accented characters in NZB names when using RSS
|
||||
- Fixed issues with accented characters in category folder names when using RSS
|
||||
- Plush: fix nzo range selection when clicking checkboxes
|
||||
- When detecting an unsupported unrar on Posix systems, drop unsupported features
|
||||
(This may lead to files being lost when duplicate names exist.)
|
||||
- Sorting examples were sometimes wrong
|
||||
- Sorting couldn't handle some pattern combinations
|
||||
- Fixed priority handling for RSS feeds and newzbin bookmarks
|
||||
- Fixed crash occuring when user profile contains characters outside ISO-8859-1
|
||||
- Allow nzb-backup folder field to be cleared
|
||||
- When using only manual newzbin bookmark reading, allow automatic un-bookmarking
|
||||
- Fixed several translation errors and omissions
|
||||
- Keep Windows awake during post-processing of last job (while queue is already empty)
|
||||
- Make it possible to run Python user scripts on OSX
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Final by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Relabeled to 0.5.0
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC8 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
- Add par2_multicore option to Config->Switches
|
||||
- Windows/OSX: Replaced OpenSSL libraries
|
||||
- Windows: Replaced par2-multicore by new version
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC7 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- When priority of newzbin or RSS job comes from the category, it wasn't effective.
|
||||
- When unpacked file and directly download file have the same name,
|
||||
the latter got lost
|
||||
- Make nzbmatrix error handling robust
|
||||
- When apikey is present in API call, do not require username/password
|
||||
- Windows installer is now multi-language too
|
||||
- Windows installer warns about queue when upgrading from 0.4.x
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC6 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Changes:
|
||||
- Add INI-only option "par2_multicore" to enable/disable par2-multicore
|
||||
Fixes:
|
||||
- Prevent crash in RSS due to odd characters
|
||||
- Prevent deadlock in downloader when server is blocked
|
||||
- Unrar option -ai should only be used on Windows
|
||||
- Prevent deletion of par files of other sets in multi-set NZBs
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC5 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Incorrect job rename in SMPL skin on Windows
|
||||
- INI-only option "no_penalties" to limit timeout penalties for
|
||||
non-optional servers to 1 minute
|
||||
- HTTPS certificate paths should default to admin_dir directive
|
||||
- The second parameter of the user script should be the original NZB file name,
|
||||
even when the job has been renamed.
|
||||
- Show clear error message when RSS feed preview or force-download fails.
|
||||
- Don't warn about RSS reading problems when a scheduled scan is done.
|
||||
- Ignore empty "username" and "apikey" parameters in nzbmatrix.com URLs
|
||||
and add stored values.
|
||||
- Suppress authentication warning for API call "get_scripts".
|
||||
It's used by some external utilities to determine if username/password is required.
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Ignore read-only attributes of files in a RAR archive
|
||||
- Remove potential crash on OSX when handling corrupt par2 file
|
||||
- Enable manual unblocking of permantly blocked servers
|
||||
- Retry permanently blocked servers after 30 minutes (compensate for shoddy servers)
|
||||
- INI-only option "folder_length_max" for trimming folder name size
|
||||
(default 128 for Windows, for others 256)
|
||||
- Give unrar preference over rar when both installed on a Posix system
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Avoid potential crash when server closes connection
|
||||
- On Connections Page: show original error message sent by server
|
||||
- Make sure unrar check isn't done for OSX
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Increase timeout for nzb fetches from URLs to 60 sec
|
||||
- Duplicate title checking in RSS is now case-insensitive
|
||||
- Only warn about presense of old queue when no new queue is present yet
|
||||
- Don't warn when a supported RAR (instead of UNRAR) program is present
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0RC1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Fix broken server test in Wizard and Config->Servers
|
||||
- Add tbbmalloc.dll to par2 program (prevents problems when you have
|
||||
another version of that file on your system)
|
||||
- Fix uneven download speed of very large NZBs
|
||||
- Include special par2 variant for OSX 10.6+ (automatically selected)
|
||||
- Fix problem with the script folder path containing a space
|
||||
- Fix disabling of RSS feeds when changing settings
|
||||
- When forcing disconnect don't try to send "Quit" to a non-responding server
|
||||
- Warn user (once) about presence of old queue file
|
||||
- Updated German texts
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta6 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Solve problems with unwilling servers and "random" server time-outs.
|
||||
- Do not generate warnings for intermittent server problems.
|
||||
- Joinable rar-sets were not always properly joined.
|
||||
- Fixed timing problems when SABnzbd is running behind an Apache server
|
||||
- Don't report failed default permissions setting when the user has set none
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta5 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
- Removed potential deadlock from Downloader
|
||||
- Allow backup servers to be used when all primaries blocked or inactive
|
||||
- Scheduled PAUSE_ALL failed to stop downloading
|
||||
- Will now continue unpacking other rar-sets when one rar-set fails to unpack
|
||||
(but only when option "Post-process Verified jobs" is off)
|
||||
- QuickPar messed up the par files of multi-set NZBs
|
||||
- When the par2 tool joined multi-part files of a multi-NZB, it mixed up files
|
||||
from other sets
|
||||
- Work-around for a bug in par2-tbb (making if fail to join some multi-part jobs).
|
||||
- When scaling down the number of connections of a server,
|
||||
will now retry with the full amount 10 minutes later.
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
* Fixed poor reliability of reading RSS feeds and subsequent NZB fetches
|
||||
(caused by a too short time-out value).
|
||||
* Posix: when user has not set a permissions value, use the account's
|
||||
default permissions (prevents problems with permissions coming from rar files)
|
||||
* When option "Post-process only verified jobs" is set, do not execute
|
||||
user-script when job fails
|
||||
* NZB files read in from categorized watched folder did not inherit
|
||||
the category's priority
|
||||
* Fixed error reporting about incorrect NZB file
|
||||
* Improved year recognition in tvsort
|
||||
* Some skin fixes
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes:
|
||||
* Solved problems with adding NZB by URL (also through RSS)
|
||||
* Proper display of download speed in OSX menu
|
||||
* Fixed compatibility of Plush and IE8
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Fixes
|
||||
* nzbmatrix.com support fixed (fetch NZB and Wizard settings)
|
||||
* Problems on some systems because of reading NZB-spec from newzbin
|
||||
for each NZB file
|
||||
* When an RSS feed has more than 9 filters, the order was disturbed
|
||||
* Fixed several TVSort problems including a hangup
|
||||
* Each RSS preview added jobs marked "will not be downloaded"
|
||||
instead of just the very first scan.
|
||||
* Email reporting disk-full was malformed
|
||||
* Sample detection was to eager to detect
|
||||
* Improve detection of 64bit versions of Windows Vista/7/Server2008
|
||||
* Explicit priorities in nzb upload were ignored
|
||||
* Corrections in translations
|
||||
Improvements
|
||||
* INI-only option to reduce the amount of admin saves (important for very large NZBs)
|
||||
* INI-only option to enable/disable final folder renaming (__UNPACK/__FAILED)
|
||||
* Fixed OSX uninstaller so that it will leave extra skins on the disk
|
||||
* Association of NZB files with SABnzbd for OSX
|
||||
* Retry folder renames and deletions on Windows
|
||||
* Don't save RSS admin to disk, when no feeds are active
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Beta1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Highlights
|
||||
* Multi-language UI: English, French, Dutch, German and Swedish
|
||||
* Start-up Wizard to get you going fast
|
||||
* QuickCheck : on-the-fly par2 check. When a download is correct,
|
||||
the normal par2 check is skipped
|
||||
* Configuration changes take immediate effect (except skins and some system folders)
|
||||
* Job priorities, individual job pausing, job renaming
|
||||
* Flexible server setup (scheduled on/off, optional servers)
|
||||
* The Plush and Smpl skins have been improved and are a lot faster than before
|
||||
and they have paging
|
||||
* New "Mobile" skin (replaces iPhone-skin)
|
||||
* RSS supports more sites and has generally improved
|
||||
* For Windows distribution: 64-bit par2 and unrar are used
|
||||
on 64-bit Vista/Win7/Server2008
|
||||
* Support for password protected rar-files (limited)
|
||||
* Expanded TV/Movie sorting
|
||||
More Tech stuff
|
||||
* HTTPS support for the Web-UI
|
||||
* Email templates to design your own emails (including multiple recipients)
|
||||
* Extensive API for utility developers
|
||||
* History survives a full queue/admin clean and future upgrades
|
||||
* Windows: optional association of NZB files with SABnzbd (double-click support)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha12 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Drop country names from language strings
|
||||
* Fixed crash when setting script to None
|
||||
* Add missing "Restart required" messages in Config->General in Classic and Smpl
|
||||
* When pause-download-while-pp is reset, resume downloading immediately
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha11 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Fix foreign character problems in several places
|
||||
* Add Swedish translation
|
||||
* Plush tweaks
|
||||
* Allow foreign characters in email templates
|
||||
* Prevent selection of "Mobile" skin as primary
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha10 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Fix foreign character problems in several places
|
||||
* top_only option wasn't changable on-the-fly
|
||||
* Windows uninstaller left behind some files
|
||||
* Complete the API with the del_config function
|
||||
* German translation
|
||||
* Plush tweaks
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha9 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Fixed bug in handling fetching of additional par2 files
|
||||
* Fixed nzbmatrix support properly
|
||||
* Fixed potential urlgrabber crash on startup
|
||||
* Fixed problem with NZB files containing space before ".nzb"
|
||||
* Fixed Windows Vista/7/Server2008 x64 detection
|
||||
* Allow feed:// syntax in RSS feeds
|
||||
* Restore 0.4.x compatibility for the RSS URL.
|
||||
* Even more Plush improvements
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha8 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Fixed failing RSS filters
|
||||
* Fixed nzbmatrix.com RSS support
|
||||
Note: you URL must contain username and ApiKey
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha7 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Use RSS titles for job names instead of downloaded filename.
|
||||
(This bypasses newzbin's filename mangling).
|
||||
* URL grabber will do 5 retries before giving up (and generate
|
||||
correct retry links in History).
|
||||
* RSS support for nzbserien.org (and slight improvement for nzbs.org)
|
||||
* Plush overhaul
|
||||
* "smpl" is now the preferred skin, "Default" renamed to "Classic".
|
||||
* Detect internal problems and restart SABnzbd if found
|
||||
* Enable "hibernate" and "standby" as end-of-queue actions for Linux systems
|
||||
(Will only work if supported by the kernel of your platform)
|
||||
* Update to unrar 3.90 (OSX and Windows)
|
||||
* Remove segment files after par2-based joining
|
||||
* Internationalisation of all texts in History
|
||||
* Internationalisation of email templates
|
||||
* Prevent crash when encountering a corrupt par2 file during QuickCheck
|
||||
* Added new api call: api?mode=warnings&name=clear
|
||||
* Added new api call: api?mode=translate&value=ACRONYM&output=json
|
||||
* Priorities in RSS feeds now work properly
|
||||
* Add "Require" filter type for RSS feeds
|
||||
* Add SSL type (V23, V2, V3), only needed for som odd ISP's
|
||||
* Add INI-file-only option to skip the check after unrar.
|
||||
This is to support people with consistently failing checks.
|
||||
* Passwords for encrypted RARs can be set in the job title
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha6 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Add 64bit unrar/rar 3.90 for Windows
|
||||
* "Mobile" skin (limited functionality)
|
||||
* Removed a number category-related problems
|
||||
* Scan enabled RSS feeds 15 sec after startup
|
||||
* Get newzbin bookmarks 20 sec after startup
|
||||
* Add scheduled pause-all command thats stops all disk activity
|
||||
* Improved handling of accented characters (also in par2)
|
||||
* Unrelated empty folders are no longer removed from "incomplete" folder
|
||||
* Initial runs of RSS will no longer be downloaded by "download" button
|
||||
instead thay are marked and can be downloaded individually
|
||||
* TV Sort: Fixed double brackets () naming on items with the country in the name.
|
||||
* Disable password completion by browsers on Config pages.
|
||||
* Retry links in History were incorrrect
|
||||
* Solved lots of small bugs
|
||||
* Skin improvements
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha5 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Less bugs
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha4 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Less bugs
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha3 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Session/API key system ported from 0.4.11
|
||||
* Multi-language, including Dutch and French translations
|
||||
* Less bugs
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha2 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
* Fixed failing delete-bookmark-on-newzbin feature.
|
||||
* Wizard improvements
|
||||
* Plush improvements
|
||||
* par2 for OSX replaced
|
||||
* Added penalty timeout for unresponsive servers (when flagged as "optional").
|
||||
* Fixed some incompatibilities with Python 2.4 and 2.6
|
||||
* TV sorting improved
|
||||
* Support for :: or [::] as valid hostnames. :: listens on all ipv6 addresses.
|
||||
* Allow non-conforming RSS feeds (fixes nzbmatrix.com issue)
|
||||
-------------------------------------------------------------------------------
|
||||
0.5.0Alpha1 by The SABnzbd-Team
|
||||
-------------------------------------------------------------------------------
|
||||
Core Stuff:
|
||||
* Updated Cherrypy - Among other things, this means you can now use HTTPS for the
|
||||
web ui and have a prettier login window if you use a login/pass.
|
||||
We have also dropped the need for /sabnzbd/ in the urls you use to access sabnzbd.
|
||||
It'll still work with /sabnzbd/, but it will also work without it now.
|
||||
* New XML Parser - Results in lower memory usage when reading .nzb files, especially large ones.
|
||||
* File Quick-Check - We can now skip par2 verification altogether in some cases by performing
|
||||
a quick-check of file hashes before post-processing.
|
||||
* New Quick-Start Wizard - If you don't have any servers set, you'll get a neat little
|
||||
five-page wizard allowing you to change all the config settings you really need to worry about.
|
||||
Stuff like "How should SABnzbd be accessible? Remotely or locally?" and "What's your server address",
|
||||
in a step by step manner. Features a button on the server page to test the connection to the news server,
|
||||
to make sure you have entered your details correctly.
|
||||
* Revamped Config System - The configuration backend was overhauled in 0.5.
|
||||
Many of the config pages have been changed around to make more sense,
|
||||
you can now easily enable/disable servers, and in general all server interaction is much faster than before.
|
||||
* E-mail Templates - The e-mail system from 0.4.x has been updated to have a full template system,
|
||||
allowing you to customize e-mail alerts.
|
||||
* File Association - .nzb files are now associated with SABnzbd, so you can just double click them
|
||||
to load the file into your queue. Currently only Windows is fully supported, however
|
||||
you can launch SABnzbd with arguments containing a path, or multiple paths to local nzb/rar/zip files.
|
||||
* .TS Filejoining - File joining has been improved to allow support for joining multiple .TS files into one file.
|
||||
* Date Sorting - To compliment series sorting, sorting has now been added for downloads with dates in their names,
|
||||
allowing you to place files in daily, monthly, yearly folders with proper naming
|
||||
* General Sorting - Sorting for general downloads allows users to expand the series sorting into
|
||||
other types of downloads. Has support for years in titles allowing files to be placed in folders
|
||||
depending on the decade.
|
||||
The API:
|
||||
* Totally overhauled for 0.5. Basically, you now have full access to near everything about
|
||||
SABnzbd via POST and XML/JSON. See the full docs for more details. Why does this matter?
|
||||
It means if you know any programming language and understand how to parse XML/JSON and POST data to an URI,
|
||||
then you can write some application which can communicate with SABnzbd almost as easily as a template can.
|
||||
The Queue & History:
|
||||
* Per-Item Pause - Now, in addition to being able to pause the whole queue, you can also pause
|
||||
individual items in the queue. You can also force downloads to start while the whole queue is paused.
|
||||
* Priorities - The queue now has four priorities, Normal, High, Low and force.
|
||||
Think of this as an easy method to move things around your queue, or to insert things into
|
||||
specific areas of your queue. One use case is to set everything to "normal" by default,
|
||||
so it works like 0.4.x. However, you can then add a new post or RSS feed as high-priority to have it be
|
||||
inserted to the top of the queue, or add one as low-priority to have it inserted at the bottom of the queue
|
||||
and keep below normal downloads that are added. Forced items will go straight to the top of the queue,
|
||||
and will continue to download even if the queue is paused.
|
||||
* New History Backend - The history is now stored in a database, so we can handle larger histories better,
|
||||
and store more information about downloads.
|
||||
Skins:
|
||||
* General Template Changes - Templates have all been updated to support all the neat new features.
|
||||
So if you're one of those guys still married to Default, don't worry about missing out on all this new stuff.
|
||||
* Plush - Complete backend rewrite to work almost exclusively off the API [so it's MUCH faster],
|
||||
and a reorganized (and more accessible!) main menu.
|
||||
* SMPL - Has been reworked to be much faster and friendlier to use.
|
||||
The default page now shows the top 5 items from the queue and history.
|
||||
The queue and history are also now pages to stop loading a massive number of items.
|
||||
Bugfixes:
|
||||
* Sure!
|
||||
@@ -1,37 +1,17 @@
|
||||
|
||||
(c) Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
(c) Copyright 2007-2011 by "The SABnzbd-team" <team@sabnzbd.org>
|
||||
|
||||
The SABnzbd-Team is:
|
||||
The SABnzbd-team is:
|
||||
|
||||
Active team:
|
||||
Safihre
|
||||
sanderjo
|
||||
jcfp
|
||||
inpheaux
|
||||
zoggy
|
||||
Sleeping members:
|
||||
ShyPike
|
||||
sw1tch
|
||||
pairofdimes
|
||||
rAf
|
||||
Honorary member (and original author):
|
||||
Gregor Kaufmann
|
||||
Current team:
|
||||
ShyPike <shypike@sabnzbd.org>
|
||||
sw1tch <switch@sabnzbd.org>
|
||||
pairofdimes <pairofdimes@sabnzbd.org>
|
||||
inpheaux <inpheaux@sabnzbd.org>
|
||||
rAf <rAf@sabnzbd.org>
|
||||
Honorary member (and original author)
|
||||
Gregor Kaufmann <tdian@users.sourceforge.net>
|
||||
|
||||
The main contributors and moderators of the translations:
|
||||
Danish: Rene (nordjyden6), Scott
|
||||
Dutch: ShyPike, Safihre
|
||||
French: rAf, Fox Ace, Fred, Morback, Jih
|
||||
German: Severin Heiniger, Tim Hartmann, DonPizza, Alex
|
||||
Norwegian: Protx, mjelva, TomP, John
|
||||
Romanian: nicusor
|
||||
Serbian: Ozzii, Krišan Darko
|
||||
Swedish: Malmis, Kim Joahnsson, Patrik-liind, Chris M
|
||||
Spanish: Syquus, Adolfo Jayme
|
||||
Portuguese (Brazil): lrrosa, diegosps
|
||||
Russian: Pavel Maryanov
|
||||
Polish: Tomasz 'Zen' Napierala
|
||||
Chinese: XsLiDian
|
||||
Finnish: Matti Ylönen
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
|
||||
100
INSTALL.txt
@@ -1,10 +1,10 @@
|
||||
SABnzbd
|
||||
SABnzbd 0.6.12
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
0) LICENSE
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
(c) Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
(c) Copyright 2007-2011 by "The SABnzbd-team" <team@sabnzbd.org>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
@@ -21,63 +21,82 @@ along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
1) INSTALL with the Windows installer
|
||||
1) INSTALL with the Win32 installer
|
||||
-------------------------------------------------------------------------------
|
||||
Just run the downloaded EXE file and the installer will start.
|
||||
It's just a simple standard installer.
|
||||
After installation, find the SABnzbd program in the Start menu and start it.
|
||||
After installaton, find the SABnzbd program in the Start menu and start it.
|
||||
|
||||
Within a few seconds your web browser will start and show the user interface.
|
||||
Within 5-10 seconds your web browser will start and show the user interface.
|
||||
Use the "Help" button in the web-interface to be directed to the Help Wiki.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
2) INSTALL pre-built Windows binaries
|
||||
2) INSTALL pre-built Win32 binaries
|
||||
-------------------------------------------------------------------------------
|
||||
Unzip pre-built version to any folder of your liking.
|
||||
Start the SABnzbd.exe program.
|
||||
Within a few seconds your web browser will start and show the user interface.
|
||||
Within 5-10 seconds your web browser will start and show the user interface.
|
||||
Use the "Help" button in the web-interface to be directed to the Help Wiki.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
3) INSTALL pre-built macOS binaries
|
||||
-------------------------------------------------------------------------------
|
||||
Download the DMG file, mount and drag the SABnzbd icon to Applications.
|
||||
Just like you do with so many apps.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
4) INSTALL with only sources
|
||||
3) INSTALL with only sources
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Specific guides to install from source are available for Windows and macOS:
|
||||
https://sabnzbd.org/wiki/installation/install-macos
|
||||
https://sabnzbd.org/wiki/installation/install-from-source-windows
|
||||
You need to have Python installed and some modules.
|
||||
|
||||
Only Python 3.9 and above is supported.
|
||||
Unix/Linux/OSX
|
||||
Python-2.5, 2.6 or 2.7 http://www.python.org
|
||||
|
||||
On Linux systems you need to install:
|
||||
par2 unrar python3-setuptools python3-pip
|
||||
OSX Leopard/SnowLeopard
|
||||
Python 2.6 http://www.activestate.com
|
||||
|
||||
OSX Lion Apple Python 2.7 (included in OSX)
|
||||
|
||||
Windows
|
||||
Python-2.5.latest http://www.activestate.com
|
||||
Python-2.6.latest
|
||||
Python-2.7.latest
|
||||
|
||||
Essential modules
|
||||
cheetah-2.0.1+ http://www.cheetahtemplate.org/
|
||||
yenc module >= 0.3 http://sabnzbd.sourceforge.net/yenc-0.3.tar.gz
|
||||
http://sabnzbd.sourceforge.net/yenc-0.3-w32fixed.zip (Win32-only)
|
||||
par2cmdline >= 0.4 http://parchive.sourceforge.net/
|
||||
http://chuchusoft.com/par2_tbb/index.html (multi-core)
|
||||
|
||||
Optional modules
|
||||
unrar >= 3.90+ http://www.rarlab.com/rar_add.htm
|
||||
unzip >= 5.52 http://www.info-zip.org/
|
||||
gnu gettext http://www.gnu.org/software/gettext/
|
||||
|
||||
Optional modules Windows
|
||||
pyopenssl >= 0.11 http://pypi.python.org/pypi/pyOpenSSL
|
||||
(Binaries, including the OpenSSL libraries)
|
||||
|
||||
Optional modules Unix/Linux/OSX
|
||||
pyopenssl >= 0.10 http://pypi.python.org/pypi/pyOpenSSL
|
||||
openssl => v0.9.8g+ http://www.openssl.org/
|
||||
Make sure the OpenSSL libraries match with PyOpenSSL
|
||||
|
||||
Embedded modules (only use the included version)
|
||||
CherryPy-3.2 rev2138 with patches http://www.cherrypy.org
|
||||
|
||||
On non-X86 platforms, for which PyPI does not provide all pre-compiled packages,
|
||||
you also need to install these development libraries (exact names might differ per platform):
|
||||
libffi-dev libssl-dev
|
||||
|
||||
Unpack the ZIP-file containing the SABnzbd sources to any folder of your liking.
|
||||
|
||||
Inside the SABnzbd source directory, install all required Python packages by running:
|
||||
python3 -m pip install -r requirements.txt
|
||||
|
||||
If you want non-English languages, you need to compile the translations.
|
||||
If you want multiple languages, you need to compile the translations.
|
||||
Start this from a shell terminal (or command prompt):
|
||||
python3 tools/make_mo.py
|
||||
python tools/make_mo.py
|
||||
|
||||
To start SABnzbd, run this from a shell terminal (or command prompt):
|
||||
python3 -OO SABnzbd.py
|
||||
|
||||
Within a few seconds your web browser will start and show the user interface.
|
||||
Start this from a shell terminal (or command prompt):
|
||||
python SABnzbd.py
|
||||
Within 5-10 seconds your web browser will start and show the user interface.
|
||||
Use the "Help" button in the web-interface to be directed to the Help Wiki.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
5) TROUBLESHOOTING
|
||||
4) TROUBLESHOOTING
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Your browser may start up with just an error page.
|
||||
@@ -86,25 +105,26 @@ This means that SABnzbd cannot use the default port 8080 to run its web-server o
|
||||
Try to use another port, you'll need to use the a command window:
|
||||
SABnzbd.exe -s localhost:7777
|
||||
or
|
||||
python3 SABnzbd.py -s localhost:7777
|
||||
python SABnzbd.py -s localhost:7777
|
||||
|
||||
You may of course try other port numbers too.
|
||||
|
||||
For troubleshooting on Windows you can use the program SABnzbd-console.exe.
|
||||
For troubleshooting you can use the program SABnzbd-console.exe.
|
||||
This will show a black window where logging information will be shown. This
|
||||
may help you solve problems easier.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
6) MORE INFORMATION
|
||||
5) MORE INFORMATION
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Visit our wiki:
|
||||
https://sabnzbd.org/wiki/
|
||||
Visit the WIKI site:
|
||||
http://wiki.sabnzbd.org/
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
7) CREDITS
|
||||
6) CREDITS
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Several parts of SABnzbd were built by other people, illustrating the
|
||||
wonderful world of Free Open Source Software.
|
||||
See the licenses folder of the main program and of the skin folders.
|
||||
See the licences folder of the main program and of the skin folders.
|
||||
|
||||
63
ISSUES.txt
@@ -2,49 +2,74 @@
|
||||
*** Known issues ***
|
||||
*******************************************
|
||||
|
||||
- To prevent unexpectedly large NZBs from eating your download quota you can set
|
||||
the option 'size_limit' on the Config->Special page.
|
||||
- To prevent unexpectedly large NZBs from eating your quotum you can set
|
||||
the option 'size_limit' in the INI file.
|
||||
Any NZB larger than this size will be set to paused and get a low priority.
|
||||
|
||||
- When par2 or unrar hang up, never just stop SABnzbd.
|
||||
Instead use your operating system's task manager to stop the par2 or unrar program.
|
||||
Forcing SABnzbd to quit may damage your queues.
|
||||
Windows-only:
|
||||
If you keep having trouble with par2 multicore you can disable it
|
||||
in Config->Switches.
|
||||
This will force the use of the old and tried, but slower par2-classic program.
|
||||
|
||||
- A bug in Windows 7 may cause severe memory leaks when you use SABnzbd in
|
||||
combination with some virus scanners and firewals.
|
||||
Install this hotfix:
|
||||
Description: http://support.microsoft.com/kb/979223/en-us
|
||||
Download location: http://support.microsoft.com/hotfix/KBHotfix.aspx?kbnum=979223&kbln=en-us
|
||||
|
||||
- Windows cannot handle pathnames longer than 254 characters.
|
||||
Currently, SABnzbd doesn't handle this problem gracefully.
|
||||
We have added the INI-only option "folder_length_max" in which you can set
|
||||
a maximum folder name size.
|
||||
For Windows the default is 128 and for others 256.
|
||||
A quite safe value for Windows would be 64.
|
||||
SABnzbd will take care of overlapping names.
|
||||
See: http://wiki.sabnzbd.org/configure-special
|
||||
|
||||
- Some Usenet servers have intermittent login (or other) problems.
|
||||
For these the server blocking method is not very favourable.
|
||||
There is an INI-only option that will limit blocks to 1 minute.
|
||||
no_penalties = 1
|
||||
See: https://sabnzbd.org/wiki/configuration/3.4/special
|
||||
See: http://wiki.sabnzbd.org/configure-special
|
||||
|
||||
- Some third-party utilities try to probe SABnzbd API in such a way that you will
|
||||
- Some third-party utilties try to probe SABnzbd API in such a way that you will
|
||||
often see warnings about unauthenticated access.
|
||||
If you are sure these probes are harmless, you can suppress the warnings by
|
||||
setting the option "api_warnings" to 0.
|
||||
See: https://sabnzbd.org/wiki/configuration/3.4/special
|
||||
setting the option "api_warnings" to 0 in the file sabnzbd.ini
|
||||
|
||||
- On macOS you may encounter downloaded files with foreign characters.
|
||||
- On OSX you may encounter downloaded files with foreign characters.
|
||||
The par2 repair may fail when the files were created on a Windows system.
|
||||
The problem is caused by the PAR2 utility and we cannot fix this now.
|
||||
This does not apply to files inside RAR files.
|
||||
|
||||
- On Linux when you download files they may have the wrong character encoding.
|
||||
You will see this only when downloaded files contain accented characters.
|
||||
You need to fix it yourself by running the convmv utility (availaible for most Linux platforms).
|
||||
|
||||
- The "Watched Folder" sometimes fails to delete the NZB files it has
|
||||
processed. This happens when other software still accesses these files.
|
||||
Some third-party utilities supporting SABnzbd are known to do this.
|
||||
We cannot solve this problem, because the Operating System (read Windows)
|
||||
prevents the removal.
|
||||
|
||||
- When SABnzbd cannot send notification emails, check your virus scanner,
|
||||
firewall or security suite. It may be blocking outgoing email.
|
||||
- Jobs deleted from the queue will leave downloaded files behind
|
||||
in the "incomplete" folder.
|
||||
You will have to remove these files manually.
|
||||
|
||||
- When you are using external drives or network shares on macOS or Linux
|
||||
- Memory usage can sometimes have high peaks. This makes using SABnzbd on very low
|
||||
memory systems (eg a SAN device) a challenge.
|
||||
|
||||
- SABnzbd is not compatible with some software firewall versions.
|
||||
The Mircosoft Windows Firewall works fine, but remember to tell this
|
||||
firewall that SABnzbd is allowed to talk to other computers.
|
||||
|
||||
- When SABnzbd cannot send nofication emails, check your virus scanner,
|
||||
firewall or securiry suite. It may be blocking outgoing email.
|
||||
|
||||
- When you are using external drives or network shares on OSX or Linux
|
||||
make sure that the drives are mounted.
|
||||
The operating system will simply redirect your files to alternative locations.
|
||||
The operating system wil simply redirect your files to alternative locations.
|
||||
You may have trouble finding the files when mounting the drive later.
|
||||
On macOS, SABnzbd will not create new folders in /Volumes.
|
||||
The result will be a failed job that can be retried once the volume has been mounted.
|
||||
|
||||
- If you use a mounted drive as "temporary download folder", it must be present when SABnzbd
|
||||
starts up. If not, SABnzbd will use the default location.
|
||||
You can make SABnzbd wait for a mount of the "temporary download folder" by setting
|
||||
Config->Special->wait_for_dfolder to 1.
|
||||
SABnzbd will appear to hang until the drive is mounted.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(c) Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
(c) Copyright 2007-2011 by "The SABnzbd-team" <team@sabnzbd.org>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
|
||||
469
NSIS_Installer.nsi
Normal file
@@ -0,0 +1,469 @@
|
||||
; -*- coding: latin-1 -*-
|
||||
;
|
||||
; Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org>
|
||||
;
|
||||
; This program is free software; you can redistribute it and/or
|
||||
; modify it under the terms of the GNU General Public License
|
||||
; as published by the Free Software Foundation; either version 2
|
||||
; of the License, or (at your option) any later version.
|
||||
;
|
||||
; This program is distributed in the hope that it will be useful,
|
||||
; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
; GNU General Public License for more details.
|
||||
;
|
||||
; You should have received a copy of the GNU General Public License
|
||||
; along with this program; if not, write to the Free Software
|
||||
; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
!addplugindir win\nsis\Plugins
|
||||
!addincludedir win\nsis\Include
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "registerExtension.nsh"
|
||||
|
||||
|
||||
Name "${SAB_PRODUCT}"
|
||||
OutFile "${SAB_FILE}"
|
||||
|
||||
|
||||
; Some default compiler settings (uncomment and change at will):
|
||||
; SetCompress auto ; (can be off or force)
|
||||
; SetDatablockOptimize on ; (can be off)
|
||||
; CRCCheck on ; (can be off)
|
||||
; AutoCloseWindow false ; (can be true for the window go away automatically at end)
|
||||
; ShowInstDetails hide ; (can be show to have them shown, or nevershow to disable)
|
||||
; SetDateSave off ; (can be on to have files restored to their orginal date)
|
||||
WindowIcon on
|
||||
|
||||
InstallDir "$PROGRAMFILES\SABnzbd"
|
||||
InstallDirRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" ""
|
||||
;DirText $(MsgSelectDir)
|
||||
|
||||
;Vista redirects $SMPROGRAMS to all users without this
|
||||
RequestExecutionLevel admin
|
||||
FileErrorText "If you have no admin rights, try to install into a user directory."
|
||||
|
||||
|
||||
;--------------------------------
|
||||
;Variables
|
||||
|
||||
Var MUI_TEMP
|
||||
Var STARTMENU_FOLDER
|
||||
;--------------------------------
|
||||
;Interface Settings
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
;Show all languages, despite user's codepage
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
|
||||
!define MUI_ICON "interfaces/Classic/templates/static/images/favicon.ico"
|
||||
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
|
||||
!insertmacro MUI_PAGE_LICENSE "LICENSE.txt"
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
;Start Menu Folder Page Configuration
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\SABnzbd"
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
|
||||
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "SABnzbd"
|
||||
;Remember the installer language
|
||||
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_LANGDLL_REGISTRY_KEY "Software\SABnzbd"
|
||||
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
|
||||
|
||||
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
|
||||
|
||||
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||
!define MUI_FINISHPAGE_RUN_TEXT $(MsgStartSab)
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote)
|
||||
;!define MUI_FINISHPAGE_LINK "View the SABnzbdPlus Wiki"
|
||||
;!define MUI_FINISHPAGE_LINK_LOCATION "http://wiki.sabnzbd.org/"
|
||||
!define MUI_FINISHPAGE_LINK $(MsgSupportUs)
|
||||
!define MUI_FINISHPAGE_LINK_LOCATION "http://www.sabnzbd.org/contribute/"
|
||||
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!define MUI_UNPAGE_COMPONENTSPAGE_NODESC
|
||||
!insertmacro MUI_UNPAGE_COMPONENTS
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;--------------------------------
|
||||
;Languages
|
||||
|
||||
; Set supported languages
|
||||
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
|
||||
!insertmacro MUI_LANGUAGE "French"
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
!insertmacro MUI_LANGUAGE "Dutch"
|
||||
!insertmacro MUI_LANGUAGE "Swedish"
|
||||
!insertmacro MUI_LANGUAGE "Danish"
|
||||
!insertmacro MUI_LANGUAGE "NORWEGIAN"
|
||||
!insertmacro MUI_LANGUAGE "Romanian"
|
||||
|
||||
|
||||
;--------------------------------
|
||||
;Reserve Files
|
||||
|
||||
;If you are using solid compression, files that are required before
|
||||
;the actual installation should be stored first in the data block,
|
||||
;because this will make your installer start faster.
|
||||
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
|
||||
|
||||
Function LaunchLink
|
||||
ExecShell "" "$INSTDIR\SABnzbd.exe"
|
||||
FunctionEnd
|
||||
|
||||
;--------------------------------
|
||||
Function .onInit
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
|
||||
;make sure sabnzbd.exe isnt running..if so abort
|
||||
loop:
|
||||
StrCpy $0 "SABnzbd.exe"
|
||||
KillProc::FindProcesses
|
||||
StrCmp $0 "0" endcheck
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgCloseSab) IDOK loop IDCANCEL exitinstall
|
||||
exitinstall:
|
||||
Abort
|
||||
endcheck:
|
||||
FunctionEnd
|
||||
|
||||
|
||||
Section "SABnzbd" SecDummy
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
IfFileExists $INSTDIR\sabnzbd.exe 0 endWarnExist
|
||||
IfFileExists $INSTDIR\locale\nl\LC_MESSAGES\SABnzbd.mo endWarnExist 0
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgOldQueue) IDOK endWarnExist IDCANCEL 0
|
||||
Abort
|
||||
endWarnExist:
|
||||
|
||||
; add files / whatever that need to be installed here.
|
||||
File /r "dist\*"
|
||||
|
||||
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "" "$INSTDIR"
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayName" "SABnzbd ${SAB_VERSION}"
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayVersion" '${SAB_VERSION}'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Publisher" 'The SABnzbd Team'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "HelpLink" 'http://forums.sabnzbd.org/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLInfoAbout" 'http://wiki.sabnzbd.org/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLUpdateInfo" 'http://sabnzbd.org/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Comments" 'The automated Usenet download tool'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayIcon" '$INSTDIR\interfaces\Classic\templates\static\images\favicon.ico'
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 18400
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoRepair" -1
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoModify" -1
|
||||
; write out uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||
|
||||
;Create shortcuts
|
||||
CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - SafeMode.lnk" "$INSTDIR\SABnzbd.exe" "--server 127.0.0.1:8080 -b1 --no-login -t Plush"
|
||||
WriteINIStr "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - Documentation.url" "InternetShortcut" "URL" "http://wiki.sabnzbd.org/"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
|
||||
|
||||
|
||||
|
||||
!insertmacro MUI_STARTMENU_WRITE_END
|
||||
|
||||
|
||||
SectionEnd ; end of default section
|
||||
|
||||
Section /o $(MsgRunAtStart) startup
|
||||
CreateShortCut "$SMPROGRAMS\Startup\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" "-b0"
|
||||
SectionEnd ;
|
||||
|
||||
Section $(MsgIcon) desktop
|
||||
CreateShortCut "$DESKTOP\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
|
||||
SectionEnd ; end of desktop icon section
|
||||
|
||||
Section /o $(MsgAssoc) assoc
|
||||
${registerExtension} "$INSTDIR\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
|
||||
;${registerExtension} "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
|
||||
SectionEnd ; end of file association section
|
||||
|
||||
; begin uninstall settings/section
|
||||
UninstallText $(MsgUninstall)
|
||||
|
||||
Section "un.$(MsgDelProgram)" Uninstall
|
||||
;make sure sabnzbd.exe isnt running..if so shut it down
|
||||
|
||||
StrCpy $0 "sabnzbd.exe"
|
||||
DetailPrint "Searching for processes called '$0'"
|
||||
KillProc::FindProcesses
|
||||
StrCmp $1 "-1" wooops
|
||||
DetailPrint "-> Found $0 processes"
|
||||
|
||||
StrCmp $0 "0" completed
|
||||
Sleep 1500
|
||||
|
||||
StrCpy $0 "sabnzbd.exe"
|
||||
DetailPrint "Killing all processes called '$0'"
|
||||
KillProc::KillProcesses
|
||||
StrCmp $1 "-1" wooops
|
||||
DetailPrint "-> Killed $0 processes, failed to kill $1 processes"
|
||||
|
||||
Goto completed
|
||||
|
||||
wooops:
|
||||
DetailPrint "-> Error: Something went wrong :-("
|
||||
Abort
|
||||
|
||||
completed:
|
||||
DetailPrint "Process Killed"
|
||||
|
||||
|
||||
; add delete commands to delete whatever files/registry keys/etc you installed here.
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd"
|
||||
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd"
|
||||
|
||||
; Delete installation files are carefully as possible
|
||||
; Using just rmdir /r "$instdir" is considered unsafe!
|
||||
Delete "$INSTDIR\email\email-de.tmpl"
|
||||
Delete "$INSTDIR\email\email-en.tmpl"
|
||||
Delete "$INSTDIR\email\email-nl.tmpl"
|
||||
Delete "$INSTDIR\email\email-fr.tmpl"
|
||||
Delete "$INSTDIR\email\email-sv.tmpl"
|
||||
Delete "$INSTDIR\email\email-da.tmpl"
|
||||
Delete "$INSTDIR\email\email-nb.tmpl"
|
||||
Delete "$INSTDIR\email\email-ro.tmpl"
|
||||
Delete "$INSTDIR\email\rss-de.tmpl"
|
||||
Delete "$INSTDIR\email\rss-en.tmpl"
|
||||
Delete "$INSTDIR\email\rss-nl.tmpl"
|
||||
Delete "$INSTDIR\email\rss-fr.tmpl"
|
||||
Delete "$INSTDIR\email\rss-sv.tmpl"
|
||||
Delete "$INSTDIR\email\rss-da.tmpl"
|
||||
Delete "$INSTDIR\email\rss-nb.tmpl"
|
||||
Delete "$INSTDIR\email\rss-ro.tmpl"
|
||||
RMDir "$INSTDIR\email"
|
||||
RMDir /r "$INSTDIR\locale"
|
||||
RMDir /r "$INSTDIR\interfaces\Classic"
|
||||
RMDir /r "$INSTDIR\interfaces\Plush"
|
||||
RMDir /r "$INSTDIR\interfaces\smpl"
|
||||
RMDir /r "$INSTDIR\interfaces\Mobile"
|
||||
RMDir /r "$INSTDIR\interfaces\wizard"
|
||||
RMDir "$INSTDIR\interfaces"
|
||||
RMDir /r "$INSTDIR\win\par2"
|
||||
RMDir /r "$INSTDIR\win\unrar"
|
||||
RMDir /r "$INSTDIR\win\unzip"
|
||||
RMDir /r "$INSTDIR\win"
|
||||
Delete "$INSTDIR\licenses\*.txt"
|
||||
Delete "$INSTDIR\licenses\Python\*.txt"
|
||||
RMDir "$INSTDIR\licenses\Python"
|
||||
RMDir "$INSTDIR\licenses"
|
||||
Delete "$INSTDIR\lib\libeay32.dll"
|
||||
Delete "$INSTDIR\lib\pywintypes25.dll"
|
||||
Delete "$INSTDIR\lib\ssleay32.dll"
|
||||
Delete "$INSTDIR\lib\sabnzbd.zip"
|
||||
Delete "$INSTDIR\lib\*.pyd"
|
||||
RMDir /r "$INSTDIR\lib\"
|
||||
RMDir /r "$INSTDIR\po\email"
|
||||
RMDir /r "$INSTDIR\po\main"
|
||||
RMDir /r "$INSTDIR\po\nsis"
|
||||
RMDIR "$INSTDIR\po"
|
||||
Delete "$INSTDIR\CHANGELOG.txt"
|
||||
Delete "$INSTDIR\COPYRIGHT.txt"
|
||||
Delete "$INSTDIR\email.tmpl"
|
||||
Delete "$INSTDIR\GPL2.txt"
|
||||
Delete "$INSTDIR\GPL3.txt"
|
||||
Delete "$INSTDIR\INSTALL.txt"
|
||||
Delete "$INSTDIR\ISSUES.txt"
|
||||
Delete "$INSTDIR\LICENSE.txt"
|
||||
Delete "$INSTDIR\nzbmatrix.txt"
|
||||
Delete "$INSTDIR\MSVCR71.dll"
|
||||
Delete "$INSTDIR\nzb.ico"
|
||||
Delete "$INSTDIR\PKG-INFO"
|
||||
Delete "$INSTDIR\python25.dll"
|
||||
Delete "$INSTDIR\python26.dll"
|
||||
Delete "$INSTDIR\README.txt"
|
||||
Delete "$INSTDIR\SABnzbd-console.exe"
|
||||
Delete "$INSTDIR\SABnzbd.exe"
|
||||
Delete "$INSTDIR\SABnzbd-helper.exe"
|
||||
Delete "$INSTDIR\SABnzbd-service.exe"
|
||||
Delete "$INSTDIR\Sample-PostProc.cmd"
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
Delete "$INSTDIR\w9xpopen.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
|
||||
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - SafeMode.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - Documentation.url"
|
||||
RMDir "$SMPROGRAMS\$MUI_TEMP"
|
||||
|
||||
Delete "$SMPROGRAMS\Startup\SABnzbd.lnk"
|
||||
|
||||
Delete "$DESKTOP\SABnzbd.lnk"
|
||||
|
||||
DeleteRegKey HKEY_CURRENT_USER "Software\SABnzbd"
|
||||
|
||||
${unregisterExtension} ".nzb" "NZB File"
|
||||
|
||||
|
||||
SectionEnd ; end of uninstall section
|
||||
|
||||
Section "un.$(MsgDelSettings)" DelSettings
|
||||
Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini"
|
||||
RMDir /r "$LOCALAPPDATA\sabnzbd\admin"
|
||||
SectionEnd
|
||||
|
||||
|
||||
Section "un.$(MsgDelLogs)" DelLogs
|
||||
RMDir /r "$LOCALAPPDATA\sabnzbd\logs"
|
||||
SectionEnd
|
||||
|
||||
|
||||
Section "un.$(MsgDelCache)" DelCache
|
||||
RMDir /r "$LOCALAPPDATA\sabnzbd\cache"
|
||||
RMDir "$LOCALAPPDATA\sabnzbd"
|
||||
SectionEnd
|
||||
|
||||
; eof
|
||||
|
||||
;--------------------------------
|
||||
;Language strings
|
||||
; MsgWarnRunning 'Please close "SABnzbd.exe" first'
|
||||
LangString MsgStartSab ${LANG_ENGLISH} "Start SABnzbd (hidden)"
|
||||
LangString MsgStartSab ${LANG_DANISH} "Start SABnzbd"
|
||||
LangString MsgStartSab ${LANG_GERMAN} "SABnzbd starten (unsichtbar)"
|
||||
LangString MsgStartSab ${LANG_FRENCH} "D<EFBFBD>marrer SABnzbd (cach<63>)"
|
||||
LangString MsgStartSab ${LANG_NORWEGIAN} "Start SABnzbd (hidden)"
|
||||
LangString MsgStartSab ${LANG_DUTCH} "Start SABnzbd (verborgen)"
|
||||
LangString MsgStartSab ${LANG_ROMANIAN} "Porneste SABnzbd (ascuns)"
|
||||
LangString MsgStartSab ${LANG_SWEDISH} "Starta SABnzbd (dold)"
|
||||
|
||||
LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes"
|
||||
LangString MsgShowRelNote ${LANG_DANISH} "Vis udgivelsesbem<65>rkninger"
|
||||
LangString MsgShowRelNote ${LANG_GERMAN} "Versionshinweise anzeigen"
|
||||
LangString MsgShowRelNote ${LANG_FRENCH} "Afficher les notes de version"
|
||||
LangString MsgShowRelNote ${LANG_NORWEGIAN} "Show Release Notes"
|
||||
LangString MsgShowRelNote ${LANG_DUTCH} "Toon vrijgave bericht"
|
||||
LangString MsgShowRelNote ${LANG_ROMANIAN} "Arat<EFBFBD> Notele de Publicare"
|
||||
LangString MsgShowRelNote ${LANG_SWEDISH} "Visa release noteringar"
|
||||
|
||||
LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!"
|
||||
LangString MsgSupportUs ${LANG_DANISH} "St<EFBFBD>tte projektet, donere!"
|
||||
LangString MsgSupportUs ${LANG_GERMAN} "Bitte unterst<73>tzen Sie das Projekt durch eine Spende!"
|
||||
LangString MsgSupportUs ${LANG_FRENCH} "Supportez le projet, faites un don !"
|
||||
LangString MsgSupportUs ${LANG_NORWEGIAN} "Support the project, Donate!"
|
||||
LangString MsgSupportUs ${LANG_DUTCH} "Steun het project, Doneer!"
|
||||
LangString MsgSupportUs ${LANG_ROMANIAN} "Sustine proiectul, Doneaz<61>!"
|
||||
LangString MsgSupportUs ${LANG_SWEDISH} "Donera och st<73>d detta projekt!"
|
||||
|
||||
LangString MsgCloseSab ${LANG_ENGLISH} "Please close $\"SABnzbd.exe$\" first"
|
||||
LangString MsgCloseSab ${LANG_DANISH} "Luk 'SABnzbd.exe' f<>rst"
|
||||
LangString MsgCloseSab ${LANG_GERMAN} "Schliessen Sie bitte zuerst $\"SABnzbd.exe$\"."
|
||||
LangString MsgCloseSab ${LANG_FRENCH} "Quittez $\"SABnzbd.exe$\" avant l'installation, SVP"
|
||||
LangString MsgCloseSab ${LANG_NORWEGIAN} "Please close $\"SABnzbd.exe$\" first"
|
||||
LangString MsgCloseSab ${LANG_DUTCH} "Sluit $\"SABnzbd.exe$\" eerst af"
|
||||
LangString MsgCloseSab ${LANG_ROMANIAN} "<EFBFBD>nchideti mai <20>nt<6E>i $\"SABnzbd.exe$\""
|
||||
LangString MsgCloseSab ${LANG_SWEDISH} "Var v<>nlig st<73>ng $\"SABnzbd.exe$\" f<>rst"
|
||||
|
||||
LangString MsgOldQueue ${LANG_ENGLISH} " >>>> WARNING <<<<$\r$\n$\r$\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
LangString MsgOldQueue ${LANG_DANISH} " >>>> WARNING <<<<$\r$\n$\r$\nVenligst, kontroll<6C>r f<>rst udgivelsesnoter eller g<> til http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
LangString MsgOldQueue ${LANG_GERMAN} " >>>> WARNUNG <<<<$\r$\n$\r$\nBitte zuerst die Versionsanmerkungen lesen oder $\"http://wiki.sabnzbd.org/introducing-0-6-0 besuchen!$\""
|
||||
LangString MsgOldQueue ${LANG_FRENCH} " >>>> AVERTISSEMENT<<<<$\r$\n$\r$\nS'il vous pla<6C>t, v<>rifiez d'abord les notes de version ou visiter http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
LangString MsgOldQueue ${LANG_NORWEGIAN} " >>>> WARNING <<<<$\r$\n$\r$\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
LangString MsgOldQueue ${LANG_DUTCH} " >>>> WAARSCHUWING <<<<$\r$\n$\r$\nLees eerst het vrijgave bericht of ga naar $\"http://wiki.sabnzbd.org/introducing-0-6-0 !$\""
|
||||
LangString MsgOldQueue ${LANG_ROMANIAN} " >>>> ATENTIE <<<<$\r$\n$\r$\nV<6E> rug<75>m, verificati mai <20>nt<6E>i notele de publicare sau mergeti la http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
LangString MsgOldQueue ${LANG_SWEDISH} " >>>> VARNING <<<<$\r$\n$\r$\nVar v<>nlig och l<>s versions noteringarna eller g<> till http://wiki.sabnzbd.org/introducing-0-6-0 !"
|
||||
|
||||
LangString MsgUninstall ${LANG_ENGLISH} "This will uninstall SABnzbd from your system"
|
||||
LangString MsgUninstall ${LANG_DANISH} "Dette vil afinstallere SABnzbd fra dit system"
|
||||
LangString MsgUninstall ${LANG_GERMAN} "Dies entfernt SABnzbd von Ihrem System"
|
||||
LangString MsgUninstall ${LANG_FRENCH} "Ceci d<>sinstallera SABnzbd de votre syst<73>me"
|
||||
LangString MsgUninstall ${LANG_NORWEGIAN} "This will uninstall SABnzbd from your system"
|
||||
LangString MsgUninstall ${LANG_DUTCH} "Dit verwijdert SABnzbd van je systeem"
|
||||
LangString MsgUninstall ${LANG_ROMANIAN} "Acest lucru va dezinstala SABnzbd din sistem"
|
||||
LangString MsgUninstall ${LANG_SWEDISH} "Detta kommer att avinstallera SABnzbd fr<66>n systemet"
|
||||
|
||||
LangString MsgRunAtStart ${LANG_ENGLISH} "Run at startup"
|
||||
LangString MsgRunAtStart ${LANG_DANISH} "K<EFBFBD>r ved opstart"
|
||||
LangString MsgRunAtStart ${LANG_GERMAN} "Beim Systemstart ausf<73>hren"
|
||||
LangString MsgRunAtStart ${LANG_FRENCH} "Lancer au d<>marrage"
|
||||
LangString MsgRunAtStart ${LANG_NORWEGIAN} "Run at startup"
|
||||
LangString MsgRunAtStart ${LANG_DUTCH} "Opstarten bij systeem start"
|
||||
LangString MsgRunAtStart ${LANG_ROMANIAN} "Executare la pornire"
|
||||
LangString MsgRunAtStart ${LANG_SWEDISH} "K<EFBFBD>r vid uppstart"
|
||||
|
||||
LangString MsgIcon ${LANG_ENGLISH} "Desktop Icon"
|
||||
LangString MsgIcon ${LANG_DANISH} "Skrivebords ikon"
|
||||
LangString MsgIcon ${LANG_GERMAN} "Desktop-Symbol"
|
||||
LangString MsgIcon ${LANG_FRENCH} "Ic<EFBFBD>ne sur le Bureau"
|
||||
LangString MsgIcon ${LANG_NORWEGIAN} "Desktop Icon"
|
||||
LangString MsgIcon ${LANG_DUTCH} "Pictogram op bureaublad"
|
||||
LangString MsgIcon ${LANG_ROMANIAN} "Icoan<EFBFBD> Desktop"
|
||||
LangString MsgIcon ${LANG_SWEDISH} "Skrivbordsikon"
|
||||
|
||||
LangString MsgAssoc ${LANG_ENGLISH} "NZB File association"
|
||||
LangString MsgAssoc ${LANG_DANISH} "NZB filtilknytning"
|
||||
LangString MsgAssoc ${LANG_GERMAN} "Mit NZB-Dateien verkn<6B>pfen"
|
||||
LangString MsgAssoc ${LANG_FRENCH} "Association des fichiers NZB"
|
||||
LangString MsgAssoc ${LANG_NORWEGIAN} "NZB File association"
|
||||
LangString MsgAssoc ${LANG_DUTCH} "NZB bestanden koppelen aan SABnzbd"
|
||||
LangString MsgAssoc ${LANG_ROMANIAN} "Asociere cu Fisierele NZB"
|
||||
LangString MsgAssoc ${LANG_SWEDISH} "NZB Filassosication"
|
||||
|
||||
LangString MsgDelProgram ${LANG_ENGLISH} "Delete Program"
|
||||
LangString MsgDelProgram ${LANG_DANISH} "Slet program"
|
||||
LangString MsgDelProgram ${LANG_GERMAN} "Programm l<>schen"
|
||||
LangString MsgDelProgram ${LANG_FRENCH} "Supprimer le programme"
|
||||
LangString MsgDelProgram ${LANG_NORWEGIAN} "Delete Program"
|
||||
LangString MsgDelProgram ${LANG_DUTCH} "Verwijder programma"
|
||||
LangString MsgDelProgram ${LANG_ROMANIAN} "Sterge Program"
|
||||
LangString MsgDelProgram ${LANG_SWEDISH} "Ta bort programmet"
|
||||
|
||||
LangString MsgDelSettings ${LANG_ENGLISH} "Delete Settings"
|
||||
LangString MsgDelSettings ${LANG_DANISH} "Slet instillinger"
|
||||
LangString MsgDelSettings ${LANG_GERMAN} "Einstellungen l<>schen"
|
||||
LangString MsgDelSettings ${LANG_FRENCH} "Supprimer les Param<61>tres"
|
||||
LangString MsgDelSettings ${LANG_NORWEGIAN} "Delete Settings"
|
||||
LangString MsgDelSettings ${LANG_DUTCH} "Verwijder instellingen"
|
||||
LangString MsgDelSettings ${LANG_ROMANIAN} "Stergeti Set<65>ri"
|
||||
LangString MsgDelSettings ${LANG_SWEDISH} "Ta bort inst<73>llningar"
|
||||
|
||||
LangString MsgDelLogs ${LANG_ENGLISH} "Delete Logs"
|
||||
LangString MsgDelLogs ${LANG_DANISH} "Slet logs"
|
||||
LangString MsgDelLogs ${LANG_GERMAN} "Protokoll l<>schen"
|
||||
LangString MsgDelLogs ${LANG_FRENCH} "Supprimer les logs"
|
||||
LangString MsgDelLogs ${LANG_NORWEGIAN} "Delete Logs"
|
||||
LangString MsgDelLogs ${LANG_DUTCH} "Verwijder logging"
|
||||
LangString MsgDelLogs ${LANG_ROMANIAN} "Stergeti Activitate"
|
||||
LangString MsgDelLogs ${LANG_SWEDISH} "Ta bort logg"
|
||||
|
||||
LangString MsgDelCache ${LANG_ENGLISH} "Delete Cache"
|
||||
LangString MsgDelCache ${LANG_DANISH} "Slet hukommelse"
|
||||
LangString MsgDelCache ${LANG_GERMAN} "Cache l<>schen"
|
||||
LangString MsgDelCache ${LANG_FRENCH} "Supprimer le Cache"
|
||||
LangString MsgDelCache ${LANG_NORWEGIAN} "Delete Cache"
|
||||
LangString MsgDelCache ${LANG_DUTCH} "Verwijder Cache"
|
||||
LangString MsgDelCache ${LANG_ROMANIAN} "Stergeti Cache"
|
||||
LangString MsgDelCache ${LANG_SWEDISH} "Ta bort tempor<6F>r-mapp"
|
||||
|
||||
Function un.onInit
|
||||
!insertmacro MUI_UNGETLANGUAGE
|
||||
FunctionEnd
|
||||
10
PKG-INFO
Normal file
@@ -0,0 +1,10 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: SABnzbd
|
||||
Version: 0.6.12
|
||||
Summary: SABnzbd-0.6.12
|
||||
Home-page: http://sourceforge.net/projects/sabnzbdplus
|
||||
Author: The SABnzbd Team
|
||||
Author-email: team@sabnzbd.org
|
||||
License: GNU General Public License 2 (GPL2 or later)
|
||||
Description: Fully automated Usenet Binary Downloader
|
||||
Platform: posix
|
||||
67
README.md
@@ -1,77 +1,54 @@
|
||||
SABnzbd - The automated Usenet download tool
|
||||
============================================
|
||||
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](https://discord.sabnzbd.org)
|
||||
|
||||
SABnzbd is an Open Source Binary Newsreader written in Python.
|
||||
|
||||
It's totally free, easy to use, and works practically everywhere.
|
||||
SABnzbd makes Usenet as simple and streamlined as possible by automating everything we can. All you have to do is add an `.nzb`. SABnzbd takes over from there, where it will be automatically downloaded, verified, repaired, extracted and filed away with zero human interaction.
|
||||
SABnzbd offers an easy setup wizard and has self-analysis tools to verify your setup.
|
||||
It's totally free, incredibly easy to use, and works practically everywhere.
|
||||
|
||||
If you want to know more you can head over to our website: https://sabnzbd.org.
|
||||
SABnzbd makes Usenet as simple and streamlined as possible by automating everything we can. All you have to do is add an .nzb. SABnzbd takes over from there, where it will be automatically downloaded, verified, repaired, extracted and filed away with zero human interaction.
|
||||
|
||||
If you want to know more you can head over to our website: http://sabnzbd.org.
|
||||
|
||||
## Resolving Dependencies
|
||||
|
||||
SABnzbd has a few dependencies you'll need before you can get running. If you've previously run SABnzbd from one of the various Linux packages, then you likely already have all the needed dependencies. If not, here's what you're looking for:
|
||||
SABnzbd has a good deal of dependencies you'll need before you can get running. If you've previously run SABnzbd from one of the various Linux packages floating around (Ubuntu, Debian, Fedora, etc), then you likely already have all the needed dependencies. If not, here's what you're looking for:
|
||||
|
||||
- `python` (Python 3.9 and above, often called `python3`)
|
||||
- Python modules listed in `requirements.txt`. Install with `python3 -m pip install -r requirements.txt -U`
|
||||
- `par2` (Multi-threaded par2 installation guide can be found [here](https://sabnzbd.org/wiki/installation/multicore-par2))
|
||||
- `unrar` (make sure you get the "official" non-free version of unrar)
|
||||
- `python` (We support Python 2.5-2.7, preferably 2.6 or 2.7.)
|
||||
- `python-cheetah`
|
||||
- `python-configobj`
|
||||
- `python-feedparser`
|
||||
- `python-dbus`
|
||||
- `python-openssl`
|
||||
- `python-support`
|
||||
- `python-yenc`
|
||||
- `par2` (Multi-threaded par2 can be downloaded from [ChuChuSoft](http://chuchusoft.com/par2_tbb/download.html) )
|
||||
- `unrar` (Make sure you get the "official" non-free version of unrar)
|
||||
- `unzip`
|
||||
|
||||
Optional:
|
||||
- See `requirements.txt`
|
||||
|
||||
Your package manager should supply these. If not, we've got links in our [installation guide](https://github.com/sabnzbd/sabnzbd/blob/master/INSTALL.txt).
|
||||
Your package manager should supply these. If not, we've got links in our more in-depth [installation guide](https://github.com/sabnzbd/sabnzbd/blob/master/INSTALL.txt).
|
||||
|
||||
## Running SABnzbd from source
|
||||
|
||||
Once you've sorted out all the dependencies, simply run:
|
||||
|
||||
```
|
||||
python3 -OO SABnzbd.py
|
||||
python SABnzbd.py
|
||||
```
|
||||
|
||||
Or, if you want to run in the background:
|
||||
|
||||
```
|
||||
python3 -OO SABnzbd.py -d -f /path/to/sabnzbd.ini
|
||||
python -d -f /path/to/sabnzbd.ini
|
||||
```
|
||||
|
||||
If you want multi-language support, run:
|
||||
|
||||
```
|
||||
python3 tools/make_mo.py
|
||||
python tools/make_mo.py
|
||||
```
|
||||
|
||||
Our many other command line options are explained in depth [here](https://sabnzbd.org/wiki/advanced/command-line-parameters).
|
||||
Our many other commandline options are explained in depth [here](http://wiki.sabnzbd.org/command-line-parameters).
|
||||
|
||||
## About Our Repo
|
||||
|
||||
The workflow we use, is a simplified form of "GitFlow".
|
||||
Basically:
|
||||
- `master` contains only stable releases (which have been merged to `master`) and is intended for end-users.
|
||||
- `develop` is the target for integration and is **not** intended for end-users.
|
||||
- `1.1.x` is a release and maintenance branch for 1.1.x (1.1.0 -> 1.1.1 -> 1.1.2) and is **not** intended for end-users.
|
||||
- `feature/my_feature` is a temporary feature branch based on `develop`.
|
||||
- `bugfix/my_bugfix` is an optional temporary branch for bugfix(es) based on `develop`.
|
||||
|
||||
Conditions:
|
||||
- Merging of a stable release into `master` will be simple: the release branch is always right.
|
||||
- `master` is not merged back to `develop`.
|
||||
- `develop` is not re-based on `master`.
|
||||
- Release branches branch from `develop` only.
|
||||
- Bugfixes created specifically for a release branch are done there (because they are specific, they're not cherry-picked to `develop`).
|
||||
- Bugfixes done on `develop` may be cherry-picked to a release branch.
|
||||
- We will not release a 1.0.2 if a 1.1.0 has already been released.
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
This program will not transfer any information to other networked systems unless
|
||||
specifically requested by the user or the person installing or operating it.
|
||||
|
||||
## Code Signing Policy
|
||||
|
||||
For our Windows release, free code signing is provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
We're going to be attempting to follow the [gitflow model](http://nvie.com/posts/a-successful-git-branching-model/), so you can consider "master" to be whatever our present stable release build is (presently 0.6.x) and "develop" to be whatever our next build will be (presently 0.7.x). Once we transition from unstable to stable dev builds we'll create release branches, and encourage you to follow along and help us test.
|
||||
52
README.mkd
@@ -1,52 +0,0 @@
|
||||
Release Notes - SABnzbd 4.6.0 Beta 2
|
||||
=========================================================
|
||||
|
||||
This is the second beta release of version 4.6.
|
||||
|
||||
## New features in 4.6.0
|
||||
|
||||
* Added support for NNTP Pipelining which eliminates idle waiting between
|
||||
requests, significantly improving speeds on high-latency connections.
|
||||
Read more here: https://sabnzbd.org/wiki/advanced/nntp-pipelining
|
||||
* Dynamically increase Assembler limits on faster connections.
|
||||
* Improved disk speed measurement in Status window.
|
||||
* Enable `verify_xff_header` by default.
|
||||
* Reduce delays between jobs during post-processing.
|
||||
* If a download only has `.nzb` files inside, the new downloads
|
||||
will include the name of the original download.
|
||||
* Dropped support for Python 3.8.
|
||||
* Windows: Added Windows ARM (portable) release.
|
||||
|
||||
## Bug fixes since 4.5.0
|
||||
|
||||
* `Check before download` could get stuck or fail to reject.
|
||||
* No error was shown in case NZB upload failed.
|
||||
* Correct mobile layout if `Full Width` is enabled.
|
||||
* Aborted Direct Unpack could result in no files being unpacked.
|
||||
* Windows: Tray icon disappears after Explorer restart.
|
||||
* macOS: Slow to start on some network setups.
|
||||
|
||||
|
||||
## Upgrade notices
|
||||
|
||||
* You can directly upgrade from version 3.0.0 and newer.
|
||||
* Upgrading from older versions will require performing a `Queue repair`.
|
||||
* Downgrading from version 4.2.0 or newer to 3.7.2 or older will require
|
||||
performing a `Queue repair` due to changes in the internal data format.
|
||||
|
||||
## Known problems and solutions
|
||||
|
||||
* Read `ISSUES.txt` or https://sabnzbd.org/wiki/introduction/known-issues
|
||||
|
||||
## Code Signing Policy
|
||||
|
||||
Windows code signing is provided by SignPath.io using a SignPath Foundation certificate.
|
||||
|
||||
## About
|
||||
|
||||
SABnzbd is an open-source cross-platform binary newsreader.
|
||||
It simplifies the process of downloading from Usenet dramatically, thanks to its web-based
|
||||
user interface and advanced built-in post-processing options that automatically verify, repair,
|
||||
extract and clean up posts downloaded from Usenet.
|
||||
|
||||
(c) Copyright 2007-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
101
README.rtf
Normal file
@@ -0,0 +1,101 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw11900\paperh16840\vieww16360\viewh15680\viewkind0
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
|
||||
|
||||
\f0\b\fs48 \cf0 SABnzbd 0.6.12\
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
|
||||
|
||||
\b0\fs26 \cf0 \
|
||||
|
||||
\b What's new (0.6.12)
|
||||
\b0 \
|
||||
- Fix issue with new localhost handling on some IPv4-only Unixes\
|
||||
- Fix job folder creation by Movie Sort when the Sort expression specifies one\
|
||||
- Fix problem with retrieving ZIP files from some web sites\
|
||||
\b What's new (0.6.11)
|
||||
\b0 \
|
||||
- Improve detection of encrypted RAR files during download\
|
||||
- SABnzbd will now listen on all "localhost" addresses\
|
||||
This should prevent problems on IPV6-enabled systems\
|
||||
- Remove unneeded extra temporary folder level in Generic Sort\
|
||||
- When par2 fails and SFV-check enabled, verify using SFV files\
|
||||
- Perform extra checks on job administration\
|
||||
- Fix logging of pre-queue script result\
|
||||
- Better support for Yahoo pipes\
|
||||
- Accept NZB files containing incorrect dates\
|
||||
- Make newzbin "Get bookmarks now" button independent of automatic readout\
|
||||
- OSX: Fix Growl issues\
|
||||
- OSX: Show the promised 10 queue entries in the OSX menu instead of 9\
|
||||
\
|
||||
\
|
||||
\b About
|
||||
\b0 \
|
||||
SABnzbd is an open-source cross-platform binary newsreader.\
|
||||
It simplifies the process of downloading from Usenet dramatically,\
|
||||
thanks to its friendly web-based user interface and advanced\
|
||||
built-in post-processing options that automatically verify, repair,\
|
||||
extract and clean up posts downloaded from Usenet.\
|
||||
SABnzbd also has a fully customizable user interface,\
|
||||
and offers a complete API for third-party applications to hook into.\
|
||||
\
|
||||
There is an extensive Wiki on the use of SABnzbd.\
|
||||
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/"}}{\fldrslt http://wiki.sabnzbd.org/}}\
|
||||
\
|
||||
|
||||
\b IMPORTANT INFORMATION
|
||||
\b0 about release 0.6.0:\
|
||||
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-6-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-6-0}}\
|
||||
\
|
||||
|
||||
\b Known problems and solutions\
|
||||
|
||||
\b0 Read the file"ISSUES.txt"
|
||||
\b \
|
||||
|
||||
\b0 \
|
||||
\
|
||||
|
||||
\b\fs40 Upgrading from 0.6.x
|
||||
\b0\fs26 \
|
||||
Stop SABnzbd.\
|
||||
Install new version\
|
||||
Start SABnzbd.\
|
||||
\
|
||||
|
||||
\b\fs40 Upgrading from 0.5.x
|
||||
\b0\fs26 \
|
||||
Stop SABnzbd.\
|
||||
Uninstall current version, keeping the data.\
|
||||
Install new version\
|
||||
Start SABnzbd.\
|
||||
\
|
||||
The organization of the download queue is different from 0.5.x.\
|
||||
0.6.x will finish downloading an existing queue, but you\
|
||||
cannot go back to an older version without losing your queue.\
|
||||
Also, your sabnzbd.ini file will be upgraded, making it\
|
||||
incompatible with release 0.5.x\
|
||||
\
|
||||
|
||||
\b\fs40 \
|
||||
Upgrading from 0.4.x
|
||||
\b0\fs26 \
|
||||
\
|
||||
|
||||
\b PLEASE DOWNLOAD YOUR CURRENT QUEUE BEFORE UPGRADING
|
||||
\b0 \
|
||||
\
|
||||
When upgrading from a 0.4.x release such as 0.4.12 your old settings will be kept.\
|
||||
You will however be given a fresh queue and history. If you have items in your queue\
|
||||
from the older version of SABnzbd, you can either re-import the nzb files if you kept\
|
||||
an nzb backup folder, or temporarily go back to 0.4.x until your queue is complete.\
|
||||
The history is now stored in a better format meaning future upgrades should be backwards\
|
||||
compatible.\
|
||||
\
|
||||
\
|
||||
|
||||
\b\fs40 Changes since 0.5.6
|
||||
\b0\fs26 \
|
||||
See: {\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-6-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-6-0}}\
|
||||
}
|
||||
30
README.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
************************ SABnzbd 0.6.12 ************************
|
||||
|
||||
What's new (0.6.12):
|
||||
- Fix issue with new localhost handling on some IPv4-only Unixes
|
||||
- Fix job folder creation by Movie Sort when the Sort expression specifies one
|
||||
- Fix problem with retrieving ZIP files from some web sites
|
||||
|
||||
What's new (0.6.11):
|
||||
- Improve detection of encrypted RAR files during download
|
||||
- SABnzbd will now listen on all "localhost" addresses
|
||||
This should prevent problems on IPV6-enabled systems
|
||||
- Remove unneeded extra temporary folder level in Generic Sort
|
||||
- When par2 fails and SFV-check enabled, verify using SFV files
|
||||
- Perform extra checks on job administration
|
||||
- Fix logging of pre-queue script result
|
||||
- Better support for Yahoo pipes
|
||||
- Accept NZB files containing incorrect dates
|
||||
- Make newzbin "Get bookmarks now" button independent of automatic readout
|
||||
- OSX: Fix Growl issues
|
||||
- OSX: Show the promised 10 queue entries in the OSX menu instead of 9
|
||||
|
||||
|
||||
About:
|
||||
SABnzbd is an open-source cross-platform binary newsreader.
|
||||
It simplifies the process of downloading from Usenet dramatically,
|
||||
thanks to its friendly web-based user interface and advanced
|
||||
built-in post-processing options that automatically verify, repair,
|
||||
extract and clean up posts downloaded from Usenet.
|
||||
|
||||
(c) Copyright 2007-2011 by "The SABnzbd-team" <team@sabnzbd.org>
|
||||
153
SABHelper.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/python -OO
|
||||
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2,5):
|
||||
print "Sorry, requires Python 2.5 or higher."
|
||||
sys.exit(1)
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
try:
|
||||
import win32api, win32file
|
||||
import win32serviceutil, win32evtlogutil, win32event, win32service, pywintypes
|
||||
except ImportError:
|
||||
print "Sorry, requires Python module PyWin32."
|
||||
sys.exit(1)
|
||||
|
||||
from util.mailslot import MailSlot
|
||||
from util.apireg import del_connection_info, set_connection_info
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
WIN_SERVICE = None
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
def HandleCommandLine(allow_service=True):
|
||||
""" Handle command line for a Windows Service
|
||||
Prescribed name that will be called by Py2Exe.
|
||||
You MUST set 'cmdline_style':'custom' in the package.py!
|
||||
"""
|
||||
win32serviceutil.HandleCommandLine(SABHelper)
|
||||
|
||||
|
||||
def start_sab():
|
||||
return subprocess.Popen('net start SABnzbd', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).stdout.read()
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
def main():
|
||||
|
||||
mail = MailSlot()
|
||||
if not mail.create(10):
|
||||
return '- Cannot create Mailslot'
|
||||
|
||||
active = False # SABnzbd should be running
|
||||
counter = 0 # Time allowed for SABnzbd to be silent
|
||||
while True:
|
||||
msg = mail.receive()
|
||||
if msg:
|
||||
if msg == 'restart':
|
||||
time.sleep(1.0)
|
||||
counter = 0
|
||||
del_connection_info(user=False)
|
||||
start_sab()
|
||||
elif msg == 'stop':
|
||||
active = False
|
||||
del_connection_info(user=False)
|
||||
elif msg == 'active':
|
||||
active = True
|
||||
counter = 0
|
||||
elif msg.startswith('api '):
|
||||
active = True
|
||||
counter = 0
|
||||
cmd, url = msg.split()
|
||||
if url:
|
||||
set_connection_info(url.strip(), user=False)
|
||||
|
||||
if active:
|
||||
counter += 1
|
||||
if counter > 120: # 120 seconds
|
||||
counter = 0
|
||||
start_sab()
|
||||
|
||||
rc = win32event.WaitForMultipleObjects((WIN_SERVICE.hWaitStop,
|
||||
WIN_SERVICE.overlapped.hEvent), 0, 1000)
|
||||
if rc == win32event.WAIT_OBJECT_0:
|
||||
del_connection_info(user=False)
|
||||
mail.disconnect()
|
||||
return ''
|
||||
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# Windows Service Support
|
||||
#
|
||||
import servicemanager
|
||||
class SABHelper(win32serviceutil.ServiceFramework):
|
||||
""" Win32 Service Handler """
|
||||
|
||||
_svc_name_ = 'SABHelper'
|
||||
_svc_display_name_ = 'SABnzbd Helper'
|
||||
_svc_deps_ = ["EventLog", "Tcpip"]
|
||||
_svc_description_ = 'Automated downloading from Usenet. ' \
|
||||
'This service helps SABnzbd to restart itself.'
|
||||
|
||||
def __init__(self, args):
|
||||
global WIN_SERVICE
|
||||
win32serviceutil.ServiceFramework.__init__(self, args)
|
||||
|
||||
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
|
||||
self.overlapped = pywintypes.OVERLAPPED()
|
||||
self.overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
|
||||
WIN_SERVICE = self
|
||||
|
||||
def SvcDoRun(self):
|
||||
msg = 'SABHelper-service'
|
||||
self.Logger(servicemanager.PYS_SERVICE_STARTED, msg + ' has started')
|
||||
res = main()
|
||||
self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + ' has stopped' + res)
|
||||
|
||||
def SvcStop(self):
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
win32event.SetEvent(self.hWaitStop)
|
||||
|
||||
def Logger(self, state, msg):
|
||||
win32evtlogutil.ReportEvent(self._svc_display_name_,
|
||||
state, 0,
|
||||
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
||||
(self._svc_name_, unicode(msg)))
|
||||
|
||||
def ErrLogger(self, msg, text):
|
||||
win32evtlogutil.ReportEvent(self._svc_display_name_,
|
||||
servicemanager.PYS_SERVICE_STOPPED, 0,
|
||||
servicemanager.EVENTLOG_ERROR_TYPE,
|
||||
(self._svc_name_, unicode(msg)),
|
||||
unicode(text))
|
||||
|
||||
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# Platform specific startup code
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
|
||||
win32serviceutil.HandleCommandLine(SABHelper, argv=sys.argv)
|
||||
2129
SABnzbd.py
@@ -1,15 +1,16 @@
|
||||
@echo off
|
||||
rem Example of a post processing script for SABnzbd
|
||||
|
||||
echo.
|
||||
echo Running in directory "%~d0%~p0"
|
||||
echo.
|
||||
echo The first parameter (result-dir) = %1
|
||||
echo The second parameter (nzb-name) = %2
|
||||
echo The third parameter (nice name) = %3
|
||||
echo The fourth parameter (newzbin #) = %4
|
||||
echo The fifth parameter (category) = %5
|
||||
echo The sixth parameter (group) = %6
|
||||
echo The seventh parameter (status) = %7
|
||||
echo The eight parameter (failure_url)= %8
|
||||
echo.
|
||||
@echo off
|
||||
rem Example of a post processing script for SABnzbd
|
||||
|
||||
echo.
|
||||
echo Running in directory "%~d0%~p0"
|
||||
echo.
|
||||
echo The first parameter (result-dir) = %1
|
||||
echo The second parameter (nzb-name) = %2
|
||||
echo The third parameter (nice name) = %3
|
||||
echo The fourth parameter (newzbin #) = %4
|
||||
echo The fifth parameter (category) = %5
|
||||
echo The sixth parameter (group) = %6
|
||||
echo The seventh parameter (status) = %7
|
||||
echo.
|
||||
|
||||
|
||||
@@ -11,4 +11,6 @@ echo "The fourth parameter (newzbin-id) =" "$4"
|
||||
echo "The fifth parameter (category) =" "$5"
|
||||
echo "The sixth parameter (group) =" "$6"
|
||||
echo "The seventh parameter (status) =" "$7"
|
||||
echo "The eight parameter (failure_url) =" "$8"
|
||||
echo
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyInstaller.building.api import EXE, COLLECT, PYZ
|
||||
from PyInstaller.building.build_main import Analysis
|
||||
from PyInstaller.building.osx import BUNDLE
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||
|
||||
from builder.constants import EXTRA_FILES, EXTRA_FOLDERS, RELEASE_VERSION, RELEASE_VERSION_TUPLE
|
||||
|
||||
# Add extra files in the PyInstaller-spec
|
||||
extra_pyinstaller_files = []
|
||||
|
||||
# Add hidden imports
|
||||
extra_hiddenimports = ["Cheetah.DummyTransaction", "cheroot.ssl.builtin", "certifi"]
|
||||
extra_hiddenimports.extend(collect_submodules("apprise"))
|
||||
extra_hiddenimports.extend(collect_submodules("babelfish.converters"))
|
||||
extra_hiddenimports.extend(collect_submodules("guessit.data"))
|
||||
|
||||
# Add platform specific stuff
|
||||
if sys.platform == "darwin":
|
||||
extra_hiddenimports.extend(["objc", "PyObjCTools"])
|
||||
# macOS folders
|
||||
EXTRA_FOLDERS += ["macos/par2/", "macos/unrar/", "macos/7zip/"]
|
||||
# Add NZB-icon file
|
||||
extra_pyinstaller_files.append(("builder/macos/image/nzbfile.icns", "."))
|
||||
# Version information is set differently on macOS
|
||||
version_info = None
|
||||
else:
|
||||
# Build would fail on non-Windows
|
||||
from PyInstaller.utils.win32.versioninfo import (
|
||||
VSVersionInfo,
|
||||
FixedFileInfo,
|
||||
StringFileInfo,
|
||||
StringTable,
|
||||
StringStruct,
|
||||
VarFileInfo,
|
||||
VarStruct,
|
||||
)
|
||||
|
||||
# Windows
|
||||
extra_hiddenimports.extend(["win32timezone", "winrt.windows.foundation.collections"])
|
||||
EXTRA_FOLDERS += ["win/par2/", "win/unrar/", "win/7zip/"]
|
||||
EXTRA_FILES += ["portable.cmd"]
|
||||
|
||||
# Detailed instructions are in the PyInstaller documentation
|
||||
# We don't include the alpha/beta/rc in the counters
|
||||
version_info = VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=RELEASE_VERSION_TUPLE,
|
||||
prodvers=RELEASE_VERSION_TUPLE,
|
||||
mask=0x3F,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0),
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
"040904B0",
|
||||
[
|
||||
StringStruct("Comments", f"SABnzbd {RELEASE_VERSION}"),
|
||||
StringStruct("CompanyName", "The SABnzbd-Team"),
|
||||
StringStruct("FileDescription", f"SABnzbd {RELEASE_VERSION}"),
|
||||
StringStruct("FileVersion", RELEASE_VERSION),
|
||||
StringStruct("LegalCopyright", "The SABnzbd-Team"),
|
||||
StringStruct("ProductName", f"SABnzbd {RELEASE_VERSION}"),
|
||||
StringStruct("ProductVersion", RELEASE_VERSION),
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
VarFileInfo([VarStruct("Translation", [1033, 1200])]),
|
||||
],
|
||||
)
|
||||
|
||||
# Process the extra-files and folders
|
||||
for file_item in EXTRA_FILES:
|
||||
extra_pyinstaller_files.append((file_item, "."))
|
||||
for folder_item in EXTRA_FOLDERS:
|
||||
extra_pyinstaller_files.append((folder_item, folder_item))
|
||||
|
||||
# Add babelfish data files
|
||||
extra_pyinstaller_files.extend(collect_data_files("babelfish"))
|
||||
extra_pyinstaller_files.extend(collect_data_files("guessit"))
|
||||
extra_pyinstaller_files.extend(collect_data_files("apprise"))
|
||||
extra_pyinstaller_files.extend(collect_data_files("dateutil"))
|
||||
|
||||
pyi_analysis = Analysis(
|
||||
["SABnzbd.py"],
|
||||
datas=extra_pyinstaller_files,
|
||||
hiddenimports=extra_hiddenimports,
|
||||
excludes=["ujson", "FixTk", "tcl", "tk", "_tkinter", "tkinter", "Tkinter", "pydoc", "pydoc_data.topics"],
|
||||
module_collection_mode={"apprise.plugins": "py"},
|
||||
)
|
||||
|
||||
pyz = PYZ(pyi_analysis.pure, pyi_analysis.zipped_data)
|
||||
|
||||
codesign_identity = os.environ.get("SIGNING_AUTH")
|
||||
if not codesign_identity:
|
||||
# PyInstaller needs specifically None, not just an empty value
|
||||
codesign_identity = None
|
||||
|
||||
# macOS specific parameters are ignored on other platforms
|
||||
exe = EXE(
|
||||
pyz,
|
||||
pyi_analysis.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="SABnzbd",
|
||||
console=False,
|
||||
append_pkg=False,
|
||||
icon="icons/sabnzbd.ico",
|
||||
contents_directory=".",
|
||||
version=version_info,
|
||||
target_arch="universal2",
|
||||
entitlements_file="builder/macos/entitlements.plist",
|
||||
codesign_identity=codesign_identity,
|
||||
)
|
||||
|
||||
coll = COLLECT(exe, pyi_analysis.binaries, pyi_analysis.zipfiles, pyi_analysis.datas, name="SABnzbd")
|
||||
|
||||
# We need to run again for the console-app
|
||||
if sys.platform == "win32":
|
||||
# Enable console=True for this one
|
||||
console_exe = EXE(
|
||||
pyz,
|
||||
pyi_analysis.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="SABnzbd-console",
|
||||
append_pkg=False,
|
||||
icon="icons/sabnzbd.ico",
|
||||
contents_directory=".",
|
||||
version=version_info,
|
||||
)
|
||||
|
||||
console_coll = COLLECT(
|
||||
console_exe,
|
||||
pyi_analysis.binaries,
|
||||
pyi_analysis.zipfiles,
|
||||
pyi_analysis.datas,
|
||||
name="SABnzbd-console",
|
||||
)
|
||||
|
||||
# Build the APP on macOS
|
||||
if sys.platform == "darwin":
|
||||
info_plist = {
|
||||
"NSUIElement": 1,
|
||||
"NSPrincipalClass": "NSApplication",
|
||||
"CFBundleShortVersionString": RELEASE_VERSION,
|
||||
"NSHumanReadableCopyright": "The SABnzbd-Team",
|
||||
"CFBundleIdentifier": "org.sabnzbd.sabnzbd",
|
||||
"CFBundleDocumentTypes": [
|
||||
{
|
||||
"CFBundleTypeExtensions": ["nzb"],
|
||||
"CFBundleTypeIconFile": "nzbfile.icns",
|
||||
"CFBundleTypeMIMETypes": ["text/nzb"],
|
||||
"CFBundleTypeName": "NZB File",
|
||||
"CFBundleTypeRole": "Viewer",
|
||||
"LSTypeIsPackage": 0,
|
||||
"NSPersistentStoreTypeKey": "Binary",
|
||||
}
|
||||
],
|
||||
"LSMinimumSystemVersion": "10.13",
|
||||
"LSEnvironment": {"LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"},
|
||||
}
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name="SABnzbd.app",
|
||||
icon="builder/macos/image/sabnzbdplus.icns",
|
||||
bundle_identifier="org.sabnzbd.sabnzbd",
|
||||
info_plist=info_plist,
|
||||
)
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2008-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
||||
# Constants
|
||||
VERSION_FILE = "sabnzbd/version.py"
|
||||
APPDATA_FILE = "linux/org.sabnzbd.sabnzbd.appdata.xml"
|
||||
|
||||
# To draft a release or not to draft a release?
|
||||
ON_GITHUB_ACTIONS = os.environ.get("CI", False)
|
||||
RELEASE_THIS = "refs/tags/" in os.environ.get("GITHUB_REF", "")
|
||||
|
||||
# Import version.py without the sabnzbd overhead
|
||||
with open(VERSION_FILE) as version_file:
|
||||
exec(version_file.read())
|
||||
RELEASE_VERSION = __version__
|
||||
|
||||
# Pre-releases are longer than 6 characters (e.g. 3.1.0Beta1 vs 3.1.0, but also 3.0.11)
|
||||
PRERELEASE = len(RELEASE_VERSION) > 5
|
||||
|
||||
# Parse the version info for Windows file properties information
|
||||
version_regexed = re.search(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)", RELEASE_VERSION)
|
||||
RELEASE_VERSION_TUPLE = (int(version_regexed.group(1)), int(version_regexed.group(2)), int(version_regexed.group(3)), 0)
|
||||
RELEASE_VERSION_BASE = f"{RELEASE_VERSION_TUPLE[0]}.{RELEASE_VERSION_TUPLE[1]}.{RELEASE_VERSION_TUPLE[2]}"
|
||||
|
||||
# Define release name
|
||||
RELEASE_NAME = "SABnzbd-%s" % RELEASE_VERSION
|
||||
RELEASE_TITLE = "SABnzbd %s" % RELEASE_VERSION
|
||||
RELEASE_SRC = RELEASE_NAME + "-src.tar.gz"
|
||||
RELEASE_WIN_BIN_X64 = RELEASE_NAME + "-win64-bin.zip"
|
||||
RELEASE_WIN_BIN_ARM64 = RELEASE_NAME + "-win-arm64-bin.zip"
|
||||
RELEASE_WIN_INSTALLER = RELEASE_NAME + "-win-setup.exe"
|
||||
RELEASE_MACOS = RELEASE_NAME + "-macos.dmg"
|
||||
RELEASE_README = "README.mkd"
|
||||
|
||||
# Detect architecture
|
||||
RELEASE_WIN_BIN = RELEASE_WIN_BIN_X64
|
||||
if platform.machine() == "ARM64":
|
||||
RELEASE_WIN_BIN = RELEASE_WIN_BIN_ARM64
|
||||
|
||||
# Used in package.py and SABnzbd.spec
|
||||
EXTRA_FILES = [
|
||||
RELEASE_README,
|
||||
"README.txt",
|
||||
"INSTALL.txt",
|
||||
"LICENSE.txt",
|
||||
"GPL2.txt",
|
||||
"GPL3.txt",
|
||||
"COPYRIGHT.txt",
|
||||
"ISSUES.txt",
|
||||
]
|
||||
EXTRA_FOLDERS = [
|
||||
"scripts/",
|
||||
"licenses/",
|
||||
"locale/",
|
||||
"email/",
|
||||
"interfaces/Glitter/",
|
||||
"interfaces/wizard/",
|
||||
"interfaces/Config/",
|
||||
"scripts/",
|
||||
"icons/",
|
||||
]
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,206 +0,0 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2008-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
from constants import RELEASE_VERSION
|
||||
|
||||
|
||||
# We need to call dmgbuild from command-line, so here we can setup how
|
||||
if __name__ == "__main__":
|
||||
# Check for DMGBuild
|
||||
try:
|
||||
import dmgbuild
|
||||
except Exception:
|
||||
print("Requires dmgbuild-module, use pip install dmgbuild")
|
||||
exit()
|
||||
|
||||
# Make sure we are in the src folder
|
||||
if not os.path.exists("builder"):
|
||||
raise FileNotFoundError("Run from the main SABnzbd source folder: python builder/package.py")
|
||||
|
||||
# Check if signing is possible
|
||||
authority = os.environ.get("SIGNING_AUTH")
|
||||
|
||||
# Extract version info and set DMG path
|
||||
# Create sub-folder to upload later
|
||||
release = RELEASE_VERSION
|
||||
prod = "SABnzbd-" + release
|
||||
fileDmg = prod + "-macos.dmg"
|
||||
|
||||
# Path to app file
|
||||
apppath = "dist/SABnzbd.app"
|
||||
|
||||
# Copy Readme
|
||||
readmepath = os.path.join(apppath, "Contents/Resources/README.txt")
|
||||
|
||||
# Path to background and the icon
|
||||
backgroundpath = "builder/macos/image/sabnzbd_new_bg.png"
|
||||
iconpath = "builder/macos/image/sabnzbdplus.icns"
|
||||
|
||||
# Make DMG
|
||||
print("Building DMG")
|
||||
dmgbuild.build_dmg(
|
||||
filename=fileDmg,
|
||||
volume_name=prod,
|
||||
settings_file="builder/make_dmg.py",
|
||||
defines={"app": apppath, "readme": readmepath, "background": backgroundpath, "iconpath": iconpath},
|
||||
)
|
||||
|
||||
# Resign APP
|
||||
if authority:
|
||||
print("Siging DMG")
|
||||
os.system('codesign --deep -f -i "org.sabnzbd.SABnzbd" -s "%s" "%s"' % (authority, fileDmg))
|
||||
print("Signed!")
|
||||
else:
|
||||
print("Signing skipped, missing SIGNING_AUTH.")
|
||||
exit()
|
||||
|
||||
|
||||
### START OF DMGBUILD SETTINGS
|
||||
### COPIED AND MODIFIED FROM THE EXAMPLE ONLINE
|
||||
application = defines.get("app", "AppName.app")
|
||||
readme = defines.get("readme", "ReadMe.rtf")
|
||||
appname = os.path.basename(application)
|
||||
|
||||
# .. Basics ....................................................................
|
||||
|
||||
# Volume format (see hdiutil create -help)
|
||||
format = defines.get("format", "UDBZ")
|
||||
|
||||
# Volume size (must be large enough for your files)
|
||||
size = defines.get("size", "100M")
|
||||
|
||||
# Files to include
|
||||
files = [application, readme]
|
||||
|
||||
# Symlinks to create
|
||||
symlinks = {"Applications": "/Applications"}
|
||||
|
||||
# Volume icon
|
||||
#
|
||||
# You can either define icon, in which case that icon file will be copied to the
|
||||
# image, *or* you can define badge_icon, in which case the icon file you specify
|
||||
# will be used to badge the system's Removable Disk icon
|
||||
#
|
||||
badge_icon = defines.get("iconpath", "")
|
||||
|
||||
# Where to put the icons
|
||||
icon_locations = {readme: (70, 160), appname: (295, 220), "Applications": (510, 220)}
|
||||
|
||||
# .. Window configuration ......................................................
|
||||
|
||||
# Window position in ((x, y), (w, h)) format
|
||||
window_rect = ((100, 100), (660, 360))
|
||||
|
||||
# Background
|
||||
#
|
||||
# This is a STRING containing any of the following:
|
||||
#
|
||||
# #3344ff - web-style RGB color
|
||||
# #34f - web-style RGB color, short form (#34f == #3344ff)
|
||||
# rgb(1,0,0) - RGB color, each value is between 0 and 1
|
||||
# hsl(120,1,.5) - HSL (hue saturation lightness) color
|
||||
# hwb(300,0,0) - HWB (hue whiteness blackness) color
|
||||
# cmyk(0,1,0,0) - CMYK color
|
||||
# goldenrod - X11/SVG named color
|
||||
# builtin-arrow - A simple built-in background with a blue arrow
|
||||
# /foo/bar/baz.png - The path to an image file
|
||||
#
|
||||
# Other color components may be expressed either in the range 0 to 1, or
|
||||
# as percentages (e.g. 60% is equivalent to 0.6).
|
||||
background = defines.get("background", "builtin-arrow")
|
||||
|
||||
show_status_bar = False
|
||||
show_tab_view = False
|
||||
show_toolbar = False
|
||||
show_pathbar = False
|
||||
show_sidebar = False
|
||||
sidebar_width = 0
|
||||
|
||||
# Select the default view; must be one of
|
||||
#
|
||||
# 'icon-view'
|
||||
# 'list-view'
|
||||
# 'column-view'
|
||||
# 'coverflow'
|
||||
#
|
||||
default_view = "icon-view"
|
||||
|
||||
# General view configuration
|
||||
show_icon_preview = False
|
||||
|
||||
# Set these to True to force inclusion of icon/list view settings (otherwise
|
||||
# we only include settings for the default view)
|
||||
include_icon_view_settings = "auto"
|
||||
include_list_view_settings = "auto"
|
||||
|
||||
# .. Icon view configuration ...................................................
|
||||
|
||||
arrange_by = None
|
||||
grid_offset = (0, 0)
|
||||
grid_spacing = 50
|
||||
scroll_position = (0, 0)
|
||||
label_pos = "bottom" # or 'right'
|
||||
text_size = 16
|
||||
icon_size = 64
|
||||
|
||||
# .. List view configuration ...................................................
|
||||
|
||||
# Column names are as follows:
|
||||
#
|
||||
# name
|
||||
# date-modified
|
||||
# date-created
|
||||
# date-added
|
||||
# date-last-opened
|
||||
# size
|
||||
# kind
|
||||
# label
|
||||
# version
|
||||
# comments
|
||||
#
|
||||
list_icon_size = 16
|
||||
list_text_size = 12
|
||||
list_scroll_position = (0, 0)
|
||||
list_sort_by = "name"
|
||||
list_use_relative_dates = True
|
||||
list_calculate_all_sizes = (False,)
|
||||
list_columns = ("name", "date-modified", "size", "kind", "date-added")
|
||||
list_column_widths = {
|
||||
"name": 300,
|
||||
"date-modified": 181,
|
||||
"date-created": 181,
|
||||
"date-added": 181,
|
||||
"date-last-opened": 181,
|
||||
"size": 97,
|
||||
"kind": 115,
|
||||
"label": 100,
|
||||
"version": 75,
|
||||
"comments": 300,
|
||||
}
|
||||
list_column_sort_directions = {
|
||||
"name": "ascending",
|
||||
"date-modified": "descending",
|
||||
"date-created": "descending",
|
||||
"date-added": "descending",
|
||||
"date-last-opened": "descending",
|
||||
"size": "descending",
|
||||
"kind": "ascending",
|
||||
"label": "ascending",
|
||||
"version": "ascending",
|
||||
"comments": "ascending",
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2008-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import configobj
|
||||
import packaging.version
|
||||
|
||||
from constants import (
|
||||
RELEASE_VERSION,
|
||||
RELEASE_VERSION_TUPLE,
|
||||
VERSION_FILE,
|
||||
RELEASE_README,
|
||||
RELEASE_NAME,
|
||||
RELEASE_WIN_BIN,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
ON_GITHUB_ACTIONS,
|
||||
RELEASE_THIS,
|
||||
RELEASE_SRC,
|
||||
EXTRA_FILES,
|
||||
EXTRA_FOLDERS,
|
||||
)
|
||||
|
||||
|
||||
# Support functions
|
||||
def safe_remove(path):
|
||||
"""Remove file without errors if the file doesn't exist
|
||||
Can also handle folders
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def delete_files_glob(glob_pattern: str, allow_no_matches: bool = False):
|
||||
"""Delete one file or set of files from wild-card spec.
|
||||
We expect to match at least 1 file, to force expected behavior"""
|
||||
if files_to_remove := glob.glob(glob_pattern):
|
||||
for path in files_to_remove:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
else:
|
||||
if not allow_no_matches:
|
||||
raise FileNotFoundError(f"No files found that match '{glob_pattern}'")
|
||||
|
||||
|
||||
def run_external_command(command: list[str], print_output: bool = True, **kwargs):
|
||||
"""Wrapper to ease the use of calling external programs"""
|
||||
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
|
||||
output, _ = process.communicate()
|
||||
ret = process.wait()
|
||||
if (output and print_output) or ret != 0:
|
||||
print(output)
|
||||
if ret != 0:
|
||||
raise RuntimeError("Command returned non-zero exit code %s!" % ret)
|
||||
return output
|
||||
|
||||
|
||||
def run_git_command(parms):
|
||||
"""Run git command, raise error if it failed"""
|
||||
return run_external_command(["git"] + parms)
|
||||
|
||||
|
||||
def patch_version_file(release_name):
|
||||
"""Patch in the Git commit hash, but only when this is
|
||||
an unmodified checkout
|
||||
"""
|
||||
git_output = run_git_command(["log", "-1"])
|
||||
for line in git_output.split("\n"):
|
||||
if "commit " in line:
|
||||
commit = line.split(" ")[1].strip()
|
||||
break
|
||||
else:
|
||||
raise TypeError("Commit hash not found")
|
||||
|
||||
with open(VERSION_FILE, "r") as ver:
|
||||
version_file = ver.read()
|
||||
|
||||
version_file = re.sub(r'__baseline__\s*=\s*"[^"]*"', '__baseline__ = "%s"' % commit, version_file)
|
||||
version_file = re.sub(r'__version__\s*=\s*"[^"]*"', '__version__ = "%s"' % release_name, version_file)
|
||||
|
||||
with open(VERSION_FILE, "w") as ver:
|
||||
ver.write(version_file)
|
||||
|
||||
|
||||
def test_macos_min_version(binary_path: str):
|
||||
# Skip check if nothing was set
|
||||
if macos_min_version := os.environ.get("MACOSX_DEPLOYMENT_TARGET"):
|
||||
# Skip any arm64 specific files
|
||||
if "arm64" in binary_path:
|
||||
print(f"Skipping arm64 binary {binary_path}")
|
||||
return
|
||||
|
||||
# Check minimum macOS version is at least mac OS10.13
|
||||
# We only check the x86_64 since for arm64 it's always macOS 11+
|
||||
print(f"Checking if binary supports macOS {macos_min_version} and above: {binary_path}")
|
||||
otool_output = run_external_command(
|
||||
[
|
||||
"otool",
|
||||
"-arch",
|
||||
"x86_64",
|
||||
"-l",
|
||||
binary_path,
|
||||
],
|
||||
print_output=False,
|
||||
)
|
||||
|
||||
# Parse the output for LC_BUILD_VERSION minos
|
||||
# The output is very large, so that's why we enumerate over it
|
||||
req_version = packaging.version.parse(macos_min_version)
|
||||
bin_version = None
|
||||
lines = otool_output.split("\n")
|
||||
for line_nr, line in enumerate(lines):
|
||||
if "LC_VERSION_MIN_MACOSX" in line:
|
||||
# Display the version in the next lines
|
||||
bin_version = packaging.version.parse(lines[line_nr + 2].split()[1])
|
||||
elif "minos" in line:
|
||||
bin_version = packaging.version.parse(line.split()[1])
|
||||
|
||||
if bin_version and bin_version > req_version:
|
||||
raise ValueError(f"{binary_path} requires {bin_version}, we want {req_version}")
|
||||
else:
|
||||
# We got the information we need
|
||||
break
|
||||
else:
|
||||
print(lines)
|
||||
raise RuntimeError(f"Could not determine minimum macOS version for {binary_path}")
|
||||
else:
|
||||
print(f"Skipping macOS version check, MACOSX_DEPLOYMENT_TARGET not set")
|
||||
|
||||
|
||||
def test_sab_binary(binary_path: str):
|
||||
"""Wrapper to have a simple start-up test for the binary"""
|
||||
with tempfile.TemporaryDirectory() as config_dir:
|
||||
sabnzbd_process = subprocess.Popen(
|
||||
[binary_path, "--browser", "0", "--logging", "2", "--config", config_dir],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
# Wait for SAB to respond
|
||||
base_url = "http://127.0.0.1:8080/"
|
||||
for _ in range(30):
|
||||
try:
|
||||
urllib.request.urlopen(base_url, timeout=1).read()
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
else:
|
||||
# Print console output and give some time to print
|
||||
print(sabnzbd_process.stdout.read())
|
||||
time.sleep(1)
|
||||
raise urllib.error.URLError("Could not connect to SABnzbd")
|
||||
|
||||
# Open a number of API calls and pages, to see if we are really up
|
||||
pages_to_test = [
|
||||
"",
|
||||
"wizard",
|
||||
"config",
|
||||
"config/server",
|
||||
"config/categories",
|
||||
"config/scheduling",
|
||||
"config/rss",
|
||||
"config/general",
|
||||
"config/folders",
|
||||
"config/switches",
|
||||
"config/sorting",
|
||||
"config/notify",
|
||||
"config/special",
|
||||
"api?mode=version",
|
||||
]
|
||||
for url in pages_to_test:
|
||||
print("Testing: %s%s" % (base_url, url))
|
||||
if b"500 Internal Server Error" in urllib.request.urlopen(base_url + url, timeout=1).read():
|
||||
raise RuntimeError("Crash in %s" % url)
|
||||
|
||||
# Parse API-key so we can do a graceful shutdown
|
||||
sab_config = configobj.ConfigObj(os.path.join(config_dir, "sabnzbd.ini"))
|
||||
urllib.request.urlopen(base_url + "shutdown/?apikey=" + sab_config["misc"]["api_key"], timeout=10)
|
||||
sabnzbd_process.wait()
|
||||
|
||||
# Print logs for verification
|
||||
with open(os.path.join(config_dir, "logs", "sabnzbd.log"), "r") as log_file:
|
||||
# Wait after printing so the output is nicely displayed in case of problems
|
||||
print(log_text := log_file.read())
|
||||
time.sleep(5)
|
||||
|
||||
# Make sure no extra errors/warnings were reported
|
||||
if "ERROR" in log_text or "WARNING" in log_text:
|
||||
raise RuntimeError("Warning or error reported during execution")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Was any option supplied?
|
||||
if len(sys.argv) < 2:
|
||||
raise TypeError("Please specify what to do")
|
||||
|
||||
# Make sure we are in the src folder
|
||||
if not os.path.exists("builder"):
|
||||
raise FileNotFoundError("Run from the main SABnzbd source folder: python builder/package.py")
|
||||
|
||||
# Check if we have the needed certificates
|
||||
try:
|
||||
import certifi
|
||||
except ImportError:
|
||||
raise FileNotFoundError("Need certifi module")
|
||||
|
||||
# Patch release file
|
||||
patch_version_file(RELEASE_VERSION)
|
||||
|
||||
# Rename release notes file
|
||||
safe_remove("README.txt")
|
||||
shutil.copyfile(RELEASE_README, "README.txt")
|
||||
|
||||
# Compile translations
|
||||
if not os.path.exists("locale"):
|
||||
run_external_command([sys.executable, "tools/make_mo.py"])
|
||||
|
||||
# Check again if translations exist, fail otherwise
|
||||
if not os.path.exists("locale"):
|
||||
raise FileNotFoundError("Failed to compile language files")
|
||||
|
||||
if "binary" in sys.argv:
|
||||
# Must be run on Windows
|
||||
if sys.platform != "win32":
|
||||
raise RuntimeError("Binary should be created on Windows")
|
||||
|
||||
# Make sure we remove any existing build-folders
|
||||
safe_remove("build")
|
||||
safe_remove("dist")
|
||||
|
||||
# Remove any leftovers
|
||||
safe_remove(RELEASE_NAME)
|
||||
safe_remove(RELEASE_WIN_BIN)
|
||||
|
||||
# Run PyInstaller and check output
|
||||
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
|
||||
run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"])
|
||||
|
||||
shutil.copytree("dist/SABnzbd-console", "dist/SABnzbd", dirs_exist_ok=True)
|
||||
safe_remove("dist/SABnzbd-console")
|
||||
|
||||
# Remove unwanted DLL's
|
||||
shutil.rmtree("dist/SABnzbd/Pythonwin")
|
||||
delete_files_glob("dist/SABnzbd/api-ms-win*.dll", allow_no_matches=True)
|
||||
delete_files_glob("dist/SABnzbd/ucrtbase.dll", allow_no_matches=True)
|
||||
|
||||
# Test the release
|
||||
test_sab_binary("dist/SABnzbd/SABnzbd.exe")
|
||||
|
||||
# Create the archive
|
||||
run_external_command(["win/7zip/7za.exe", "a", RELEASE_WIN_BIN, "SABnzbd"], cwd="dist")
|
||||
shutil.move(f"dist/{RELEASE_WIN_BIN}", RELEASE_WIN_BIN)
|
||||
|
||||
if "installer" in sys.argv:
|
||||
# Check if we have the dist folder
|
||||
if not os.path.exists("dist/SABnzbd/SABnzbd.exe"):
|
||||
raise FileNotFoundError("SABnzbd executable not found, run binary creation first")
|
||||
|
||||
# Check if we have a signed version
|
||||
if os.path.exists(f"signed/{RELEASE_WIN_BIN}"):
|
||||
print("Using signed version of SABnzbd binaries")
|
||||
safe_remove("dist/SABnzbd")
|
||||
run_external_command(["win/7zip/7za.exe", "x", "-odist", f"signed/{RELEASE_WIN_BIN}"])
|
||||
|
||||
# Make sure it exists
|
||||
if not os.path.exists("dist/SABnzbd/SABnzbd.exe"):
|
||||
raise FileNotFoundError("SABnzbd executable not found, signed zip extraction failed")
|
||||
elif RELEASE_THIS:
|
||||
raise FileNotFoundError("Signed SABnzbd executable not found, required for release!")
|
||||
else:
|
||||
print("Using unsigned version of SABnzbd binaries")
|
||||
|
||||
# Compile NSIS translations
|
||||
safe_remove("NSIS_Installer.nsi")
|
||||
safe_remove("NSIS_Installer.nsi.tmp")
|
||||
shutil.copyfile("builder/win/NSIS_Installer.nsi", "NSIS_Installer.nsi")
|
||||
run_external_command([sys.executable, "tools/make_mo.py", "nsis"])
|
||||
|
||||
# Run NSIS to build installer
|
||||
run_external_command(
|
||||
[
|
||||
"makensis.exe",
|
||||
"/V3",
|
||||
"/DSAB_VERSION=%s" % RELEASE_VERSION,
|
||||
"/DSAB_VERSIONKEY=%s" % ".".join(map(str, RELEASE_VERSION_TUPLE)),
|
||||
"/DSAB_FILE=%s" % RELEASE_WIN_INSTALLER,
|
||||
"NSIS_Installer.nsi.tmp",
|
||||
]
|
||||
)
|
||||
|
||||
if "app" in sys.argv:
|
||||
# Must be run on macOS
|
||||
if sys.platform != "darwin":
|
||||
raise RuntimeError("App should be created on macOS")
|
||||
|
||||
# Who will sign and notarize this?
|
||||
authority = os.environ.get("SIGNING_AUTH")
|
||||
notarization_user = os.environ.get("NOTARIZATION_USER")
|
||||
notarization_pass = os.environ.get("NOTARIZATION_PASS")
|
||||
|
||||
# We need to sign all the included binaries before packaging them
|
||||
# Otherwise the signature of the main application becomes invalid
|
||||
if authority:
|
||||
files_to_sign = [
|
||||
"macos/par2/par2",
|
||||
"macos/unrar/unrar",
|
||||
"macos/unrar/arm64/unrar",
|
||||
"macos/7zip/7zz",
|
||||
]
|
||||
for file_to_sign in files_to_sign:
|
||||
# Make sure it supports the macOS versions we want first
|
||||
test_macos_min_version(file_to_sign)
|
||||
|
||||
# Then sign in
|
||||
print("Signing %s with hardened runtime" % file_to_sign)
|
||||
run_external_command(
|
||||
[
|
||||
"codesign",
|
||||
"--deep",
|
||||
"--force",
|
||||
"--timestamp",
|
||||
"--options",
|
||||
"runtime",
|
||||
"--entitlements",
|
||||
"builder/macos/entitlements.plist",
|
||||
"-s",
|
||||
authority,
|
||||
file_to_sign,
|
||||
],
|
||||
print_output=False,
|
||||
)
|
||||
print("Signed %s!" % file_to_sign)
|
||||
|
||||
# Run PyInstaller and check output
|
||||
shutil.copyfile("builder/SABnzbd.spec", "SABnzbd.spec")
|
||||
run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"])
|
||||
|
||||
# Make sure we created a fully universal2 release when releasing or during CI
|
||||
if RELEASE_THIS or ON_GITHUB_ACTIONS:
|
||||
for bin_to_check in glob.glob("dist/SABnzbd.app/**/*.so", recursive=True):
|
||||
print("Checking if binary is universal2: %s" % bin_to_check)
|
||||
file_output = run_external_command(["file", bin_to_check], print_output=False)
|
||||
# Make sure we have both arm64 and x86
|
||||
if not ("x86_64" in file_output and "arm64" in file_output):
|
||||
raise RuntimeError("Non-universal2 binary found!")
|
||||
|
||||
# Make sure it supports the macOS versions we want
|
||||
test_macos_min_version(bin_to_check)
|
||||
|
||||
# Only continue if we can sign
|
||||
if authority:
|
||||
# We use PyInstaller to sign the main SABnzbd executable and the SABnzbd.app
|
||||
files_already_signed = [
|
||||
"dist/SABnzbd.app/Contents/MacOS/SABnzbd",
|
||||
"dist/SABnzbd.app",
|
||||
]
|
||||
for file_to_check in files_already_signed:
|
||||
print("Checking signature of %s" % file_to_check)
|
||||
sign_result = run_external_command(
|
||||
[
|
||||
"codesign",
|
||||
"-dv",
|
||||
"-r-",
|
||||
file_to_check,
|
||||
],
|
||||
print_output=False,
|
||||
) + run_external_command(
|
||||
[
|
||||
"codesign",
|
||||
"--verify",
|
||||
"--deep",
|
||||
file_to_check,
|
||||
],
|
||||
print_output=False,
|
||||
)
|
||||
if authority not in sign_result or "adhoc" in sign_result or "invalid" in sign_result:
|
||||
raise RuntimeError("Signature of %s seems invalid!" % file_to_check)
|
||||
|
||||
# Always notarize, as newer macOS versions don't allow any code without it
|
||||
if notarization_user and notarization_pass:
|
||||
# Prepare zip to upload to notarization service
|
||||
print("Creating zip to send to Apple notarization service")
|
||||
# We need to use ditto, otherwise the signature gets lost!
|
||||
notarization_zip = RELEASE_NAME + ".zip"
|
||||
run_external_command(
|
||||
["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", "dist/SABnzbd.app", notarization_zip]
|
||||
)
|
||||
|
||||
# Upload to Apple
|
||||
print("Sending zip to Apple notarization service")
|
||||
upload_result = run_external_command(
|
||||
[
|
||||
"xcrun",
|
||||
"notarytool",
|
||||
"submit",
|
||||
notarization_zip,
|
||||
"--apple-id",
|
||||
notarization_user,
|
||||
"--team-id",
|
||||
authority,
|
||||
"--password",
|
||||
notarization_pass,
|
||||
"--wait",
|
||||
],
|
||||
)
|
||||
|
||||
# Check if success
|
||||
if "status: accepted" not in upload_result.lower():
|
||||
raise RuntimeError("Failed to notarize..")
|
||||
|
||||
# Staple the notarization!
|
||||
print("Approved! Stapling the result to the app")
|
||||
run_external_command(["xcrun", "stapler", "staple", "dist/SABnzbd.app"])
|
||||
else:
|
||||
print("Notarization skipped, NOTARIZATION_USER or NOTARIZATION_PASS missing.")
|
||||
else:
|
||||
print("Signing skipped, missing SIGNING_AUTH.")
|
||||
|
||||
# Test the release, as the very last step to not mess with any release code
|
||||
test_sab_binary("dist/SABnzbd.app/Contents/MacOS/SABnzbd")
|
||||
|
||||
if "source" in sys.argv:
|
||||
# Prepare Source distribution package.
|
||||
# We assume the sources are freshly cloned from the repo
|
||||
# Make sure all source files are Unix format
|
||||
src_folder = "srcdist"
|
||||
safe_remove(src_folder)
|
||||
os.mkdir(src_folder)
|
||||
|
||||
# Remove any leftovers
|
||||
safe_remove(RELEASE_SRC)
|
||||
|
||||
# Add extra files and folders need for source dist
|
||||
EXTRA_FOLDERS.extend(["sabnzbd/", "po/", "linux/", "tools/", "tests/"])
|
||||
EXTRA_FILES.extend(["SABnzbd.py", "requirements.txt"])
|
||||
|
||||
# Copy all folders and files to the new folder
|
||||
for source_folder in EXTRA_FOLDERS:
|
||||
shutil.copytree(source_folder, os.path.join(src_folder, source_folder), dirs_exist_ok=True)
|
||||
|
||||
# Copy all files
|
||||
for source_file in EXTRA_FILES:
|
||||
shutil.copyfile(source_file, os.path.join(src_folder, source_file))
|
||||
|
||||
# Make sure all line-endings are correct
|
||||
for input_filename in glob.glob("%s/**/*.*" % src_folder, recursive=True):
|
||||
base, ext = os.path.splitext(input_filename)
|
||||
if ext.lower() not in (".py", ".txt", ".css", ".js", ".tmpl", ".sh", ".cmd"):
|
||||
continue
|
||||
print(input_filename)
|
||||
|
||||
with open(input_filename, "rb") as input_data:
|
||||
data = input_data.read()
|
||||
data = data.replace(b"\r", b"")
|
||||
with open(input_filename, "wb") as output_data:
|
||||
output_data.write(data)
|
||||
|
||||
# Create tar.gz file for source distro
|
||||
with tarfile.open(RELEASE_SRC, "w:gz") as tar_output:
|
||||
for root, dirs, files in os.walk(src_folder):
|
||||
for _file in files:
|
||||
input_path = os.path.join(root, _file)
|
||||
if sys.platform == "win32":
|
||||
tar_path = input_path.replace("srcdist\\", RELEASE_NAME + "/").replace("\\", "/")
|
||||
else:
|
||||
tar_path = input_path.replace("srcdist/", RELEASE_NAME + "/")
|
||||
tarinfo = tar_output.gettarinfo(input_path, tar_path)
|
||||
tarinfo.uid = 0
|
||||
tarinfo.gid = 0
|
||||
if _file in ("SABnzbd.py", "Sample-PostProc.sh", "make_mo.py", "msgfmt.py"):
|
||||
# Force Linux/macOS scripts as executable
|
||||
tarinfo.mode = 0o755
|
||||
else:
|
||||
tarinfo.mode = 0o644
|
||||
|
||||
with open(input_path, "rb") as f:
|
||||
tar_output.addfile(tarinfo, f)
|
||||
|
||||
# Remove source folder
|
||||
safe_remove(src_folder)
|
||||
|
||||
# Reset!
|
||||
run_git_command(["reset", "--hard"])
|
||||
run_git_command(["clean", "-f"])
|
||||
@@ -1,2 +0,0 @@
|
||||
PyGithub==2.8.1
|
||||
praw==7.8.1
|
||||
@@ -1,272 +0,0 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2008-2025 by The SABnzbd-Team (sabnzbd.org)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import github
|
||||
import praw
|
||||
|
||||
from constants import (
|
||||
RELEASE_VERSION,
|
||||
RELEASE_VERSION_BASE,
|
||||
PRERELEASE,
|
||||
RELEASE_SRC,
|
||||
RELEASE_WIN_BIN_X64,
|
||||
RELEASE_WIN_BIN_ARM64,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
RELEASE_MACOS,
|
||||
RELEASE_README,
|
||||
RELEASE_THIS,
|
||||
RELEASE_TITLE,
|
||||
APPDATA_FILE,
|
||||
ON_GITHUB_ACTIONS,
|
||||
)
|
||||
|
||||
# Verify we have all assets
|
||||
files_to_check = (
|
||||
RELEASE_SRC,
|
||||
RELEASE_WIN_BIN_X64,
|
||||
RELEASE_WIN_BIN_ARM64,
|
||||
RELEASE_WIN_INSTALLER,
|
||||
RELEASE_MACOS,
|
||||
RELEASE_README,
|
||||
)
|
||||
for file_to_check in files_to_check:
|
||||
if not os.path.exists(file_to_check):
|
||||
raise RuntimeError("Not all release files are present!")
|
||||
print("All release files are present")
|
||||
|
||||
# Verify that appdata file is updated
|
||||
if not isinstance(ET.parse(APPDATA_FILE).find(f"./releases/release[@version='{RELEASE_VERSION_BASE}']"), ET.Element):
|
||||
release_missing = f"Could not find {RELEASE_VERSION_BASE} in {APPDATA_FILE}"
|
||||
if RELEASE_THIS:
|
||||
raise RuntimeError(release_missing)
|
||||
elif ON_GITHUB_ACTIONS:
|
||||
print(f"::warning file={APPDATA_FILE},title=Missing release::{release_missing}")
|
||||
else:
|
||||
print(release_missing)
|
||||
|
||||
# Calculate hashes for Synology release
|
||||
with open(RELEASE_SRC, "rb") as inp_file:
|
||||
source_data = inp_file.read()
|
||||
|
||||
print("---- Synology spksrc digest hashes ---- ")
|
||||
print(RELEASE_SRC, "SHA1", hashlib.sha1(source_data).hexdigest())
|
||||
print(RELEASE_SRC, "SHA256", hashlib.sha256(source_data).hexdigest())
|
||||
print(RELEASE_SRC, "MD5", hashlib.md5(source_data).hexdigest())
|
||||
print("----")
|
||||
|
||||
# Check if tagged as release and check for token
|
||||
gh_token = os.environ.get("AUTOMATION_GITHUB_TOKEN", "")
|
||||
if RELEASE_THIS and gh_token:
|
||||
gh_obj = github.Github(auth=github.Auth.Token(gh_token))
|
||||
gh_repo = gh_obj.get_repo("sabnzbd/sabnzbd")
|
||||
|
||||
# Read the release notes
|
||||
with open(RELEASE_README, "r") as readme_file:
|
||||
readme_data = readme_file.read()
|
||||
|
||||
# We have to manually check if we already created this release
|
||||
for release in gh_repo.get_releases():
|
||||
if release.tag_name == RELEASE_VERSION:
|
||||
gh_release = release
|
||||
print("Found existing release %s" % gh_release.name)
|
||||
break
|
||||
else:
|
||||
# Did not find it, so create the release, use the GitHub tag we got as input
|
||||
print("Creating GitHub release SABnzbd %s" % RELEASE_VERSION)
|
||||
gh_release = gh_repo.create_git_release(
|
||||
tag=RELEASE_VERSION,
|
||||
name=RELEASE_TITLE,
|
||||
message=readme_data,
|
||||
draft=True,
|
||||
prerelease=PRERELEASE,
|
||||
)
|
||||
|
||||
# Fetch existing assets, as overwriting is not allowed by GitHub
|
||||
gh_assets = gh_release.get_assets()
|
||||
|
||||
# Upload the assets
|
||||
for file_to_check in files_to_check:
|
||||
if os.path.exists(file_to_check):
|
||||
# Check if this file was previously uploaded
|
||||
if gh_assets.totalCount:
|
||||
for gh_asset in gh_assets:
|
||||
if gh_asset.name == file_to_check:
|
||||
print("Removing existing asset %s " % gh_asset.name)
|
||||
gh_asset.delete_asset()
|
||||
# Upload the new one
|
||||
print("Uploading %s to release %s" % (file_to_check, gh_release.name))
|
||||
gh_release.upload_asset(file_to_check)
|
||||
|
||||
# Check if we now have all files
|
||||
gh_new_assets = gh_release.get_assets()
|
||||
if gh_new_assets.totalCount:
|
||||
all_assets = [gh_asset.name for gh_asset in gh_new_assets]
|
||||
|
||||
# Check if we have all files, using set-comparison
|
||||
if set(files_to_check) == set(all_assets):
|
||||
print("All assets present, releasing %s" % RELEASE_VERSION)
|
||||
# Publish release
|
||||
gh_release.update_release(
|
||||
tag_name=RELEASE_VERSION,
|
||||
name=RELEASE_TITLE,
|
||||
message=readme_data,
|
||||
draft=False,
|
||||
prerelease=PRERELEASE,
|
||||
)
|
||||
|
||||
# Update the website
|
||||
gh_repo_web = gh_obj.get_repo("sabnzbd/sabnzbd.github.io")
|
||||
# Check if the branch already exists, only create one if it doesn't
|
||||
skip_website_update = False
|
||||
try:
|
||||
gh_repo_web.get_branch(RELEASE_VERSION)
|
||||
print("Branch %s on sabnzbd/sabnzbd.github.io already exists, skipping update" % RELEASE_VERSION)
|
||||
skip_website_update = True
|
||||
except github.GithubException:
|
||||
# Create a new branch to have the changes
|
||||
sb = gh_repo_web.get_branch("master")
|
||||
print("Creating branch %s on sabnzbd/sabnzbd.github.io" % RELEASE_VERSION)
|
||||
new_branch = gh_repo_web.create_git_ref(ref="refs/heads/" + RELEASE_VERSION, sha=sb.commit.sha)
|
||||
|
||||
# Update the files
|
||||
if not skip_website_update:
|
||||
# We need bytes version to interact with GitHub
|
||||
RELEASE_VERSION_BYTES = RELEASE_VERSION.encode()
|
||||
|
||||
# Get all the version files
|
||||
latest_txt = gh_repo_web.get_contents("latest.txt")
|
||||
latest_txt_items = latest_txt.decoded_content.split()
|
||||
new_latest_txt_items = latest_txt_items[:2]
|
||||
config_yml = gh_repo_web.get_contents("_config.yml")
|
||||
if PRERELEASE:
|
||||
# If it's a pre-release, we append to current version in latest.txt
|
||||
new_latest_txt_items.extend([RELEASE_VERSION_BYTES, latest_txt_items[1]])
|
||||
# And replace in _config.yml
|
||||
new_config_yml = re.sub(
|
||||
b"latest_testing: '[^']*'",
|
||||
b"latest_testing: '%s'" % RELEASE_VERSION_BYTES,
|
||||
config_yml.decoded_content,
|
||||
)
|
||||
else:
|
||||
# New stable release, replace the version
|
||||
new_latest_txt_items[0] = RELEASE_VERSION_BYTES
|
||||
# And replace in _config.yml
|
||||
new_config_yml = re.sub(
|
||||
b"latest_testing: '[^']*'",
|
||||
b"latest_testing: ''",
|
||||
config_yml.decoded_content,
|
||||
)
|
||||
new_config_yml = re.sub(
|
||||
b"latest_stable: '[^']*'",
|
||||
b"latest_stable: '%s'" % RELEASE_VERSION_BYTES,
|
||||
new_config_yml,
|
||||
)
|
||||
# Also update the wiki-settings, these only use x.x notation
|
||||
new_config_yml = re.sub(
|
||||
b"wiki_version: '[^']*'",
|
||||
b"wiki_version: '%s'" % RELEASE_VERSION_BYTES[:3],
|
||||
new_config_yml,
|
||||
)
|
||||
|
||||
# Update the files
|
||||
print("Updating latest.txt")
|
||||
gh_repo_web.update_file(
|
||||
"latest.txt",
|
||||
"Release %s: latest.txt" % RELEASE_VERSION,
|
||||
b"\n".join(new_latest_txt_items),
|
||||
latest_txt.sha,
|
||||
RELEASE_VERSION,
|
||||
)
|
||||
print("Updating _config.yml")
|
||||
gh_repo_web.update_file(
|
||||
"_config.yml",
|
||||
"Release %s: _config.yml" % RELEASE_VERSION,
|
||||
new_config_yml,
|
||||
config_yml.sha,
|
||||
RELEASE_VERSION,
|
||||
)
|
||||
|
||||
# Create pull-request
|
||||
print("Creating pull request in sabnzbd/sabnzbd.github.io for the update")
|
||||
update_pr = gh_repo_web.create_pull(
|
||||
title="Release %s" % RELEASE_VERSION,
|
||||
base="master",
|
||||
body="Automated update of release files",
|
||||
head=RELEASE_VERSION,
|
||||
)
|
||||
|
||||
# Merge pull-request
|
||||
print("Merging pull request in sabnzbd/sabnzbd.github.io for the update")
|
||||
update_pr.merge(merge_method="squash")
|
||||
|
||||
# Only with GitHub success we proceed to Reddit
|
||||
if reddit_token := os.environ.get("REDDIT_TOKEN", ""):
|
||||
# Token format (without whitespace):
|
||||
# {
|
||||
# "client_id":"XXX",
|
||||
# "client_secret":"XXX",
|
||||
# "user_agent":"SABnzbd release script",
|
||||
# "username":"Safihre",
|
||||
# "password":"XXX"
|
||||
# }
|
||||
credentials = json.loads(reddit_token)
|
||||
reddit = praw.Reddit(**credentials)
|
||||
|
||||
subreddit_sabnzbd = reddit.subreddit("sabnzbd")
|
||||
subreddit_usenet = reddit.subreddit("usenet")
|
||||
|
||||
# Read the release notes
|
||||
with open(RELEASE_README, "r") as readme_file:
|
||||
readme_lines = readme_file.readlines()
|
||||
|
||||
# Put the download link after the title
|
||||
readme_lines[2] = "## https://sabnzbd.org/downloads\n\n"
|
||||
|
||||
# Use the header in the readme as title
|
||||
title = readme_lines[0]
|
||||
release_notes_text = "".join(readme_lines[2:])
|
||||
print("Posting release notes to Reddit")
|
||||
|
||||
# Only stable releases to r/usenet
|
||||
if not PRERELEASE:
|
||||
# Get correct flair-id (required by r/usenet)
|
||||
for flair in subreddit_usenet.flair.link_templates.user_selectable():
|
||||
if flair["flair_text"] == "News":
|
||||
print("Posting to r/usenet")
|
||||
submission = subreddit_usenet.submit(
|
||||
title, selftext=release_notes_text, flair_id=flair["flair_template_id"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise ValueError("Could not locate flair_text for posting to r/usenet")
|
||||
|
||||
# Post always to r/SABnzbd
|
||||
print("Posting to r/sabnzbd")
|
||||
subreddit_sabnzbd.submit(title, selftext=release_notes_text)
|
||||
|
||||
else:
|
||||
print("Missing REDDIT_TOKEN")
|
||||
|
||||
else:
|
||||
print("To push release to GitHub, first tag the commit.")
|
||||
print("Or missing the AUTOMATION_GITHUB_TOKEN, cannot push to GitHub without it.")
|
||||
@@ -1,19 +0,0 @@
|
||||
# Basic build requirements
|
||||
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
|
||||
pyinstaller==6.17.0
|
||||
packaging==25.0
|
||||
pyinstaller-hooks-contrib==2025.10
|
||||
altgraph==0.17.5
|
||||
wrapt==2.0.1
|
||||
setuptools==80.9.0
|
||||
|
||||
# For the Windows build
|
||||
pefile==2024.8.26; sys_platform == 'win32'
|
||||
pywin32-ctypes==0.2.3; sys_platform == 'win32'
|
||||
|
||||
# For the macOS build
|
||||
dmgbuild==1.6.6; sys_platform == 'darwin'
|
||||
mac-alias==2.2.3; sys_platform == 'darwin'
|
||||
macholib==1.16.4; sys_platform == 'darwin'
|
||||
ds-store==1.3.2; sys_platform == 'darwin'
|
||||
PyNaCl==1.6.1; sys_platform == 'darwin'
|
||||
@@ -1,442 +0,0 @@
|
||||
; -*- coding: utf-8 -*-
|
||||
;
|
||||
; Copyright 2008-2015 The SABnzbd-Team (sabnzbd.org)
|
||||
;
|
||||
; This program is free software; you can redistribute it and/or
|
||||
; modify it under the terms of the GNU General Public License
|
||||
; as published by the Free Software Foundation; either version 2
|
||||
; of the License, or (at your option) any later version.
|
||||
;
|
||||
; This program is distributed in the hope that it will be useful,
|
||||
; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
; GNU General Public License for more details.
|
||||
;
|
||||
; You should have received a copy of the GNU General Public License
|
||||
; along with this program; if not, write to the Free Software
|
||||
; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
Unicode true
|
||||
|
||||
!addplugindir builder\win\nsis\Plugins
|
||||
!addincludedir builder\win\nsis\Include
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "registerExtension.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "nsProcess.nsh"
|
||||
!include "x64.nsh"
|
||||
!include "servicelib.nsh"
|
||||
!include "StdUtils.nsh"
|
||||
|
||||
;------------------------------------------------------------------
|
||||
;
|
||||
; Macro for removing existing and the current installation
|
||||
; It shared by the installer and the uninstaller.
|
||||
;
|
||||
!define RemovePrev "!insertmacro RemovePrev"
|
||||
!macro RemovePrev idir
|
||||
; Remove the whole dir
|
||||
; Users should not be putting stuff here!
|
||||
RMDir /r "${idir}"
|
||||
!macroend
|
||||
|
||||
!define RemovePrevShortcuts "!insertmacro RemovePrevShortcuts"
|
||||
!macro RemovePrevShortcuts
|
||||
; Remove shortcuts, starting with current user ones (from old installs)
|
||||
SetShellVarContext current
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - SafeMode.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - Documentation.url"
|
||||
RMDir "$SMPROGRAMS\$MUI_TEMP"
|
||||
Delete "$SMPROGRAMS\Startup\SABnzbd.lnk"
|
||||
Delete "$DESKTOP\SABnzbd.lnk"
|
||||
|
||||
SetShellVarContext all
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - SafeMode.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - Documentation.url"
|
||||
RMDir "$SMPROGRAMS\$MUI_TEMP"
|
||||
Delete "$SMPROGRAMS\Startup\SABnzbd.lnk"
|
||||
Delete "$DESKTOP\SABnzbd.lnk"
|
||||
!macroend
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Define names of the product
|
||||
Name "SABnzbd ${SAB_VERSION}"
|
||||
VIProductVersion "${SAB_VERSIONKEY}"
|
||||
VIFileVersion "${SAB_VERSIONKEY}"
|
||||
|
||||
VIAddVersionKey "Comments" "SABnzbd ${SAB_VERSION}"
|
||||
VIAddVersionKey "CompanyName" "The SABnzbd-Team"
|
||||
VIAddVersionKey "FileDescription" "SABnzbd ${SAB_VERSION}"
|
||||
VIAddVersionKey "FileVersion" "${SAB_VERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "The SABnzbd-Team"
|
||||
VIAddVersionKey "ProductName" "SABnzbd ${SAB_VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${SAB_VERSION}"
|
||||
|
||||
OutFile "${SAB_FILE}"
|
||||
InstallDir "$PROGRAMFILES\SABnzbd"
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Some default compiler settings (uncomment and change at will):
|
||||
SetCompress auto ; (can be off or force)
|
||||
SetDatablockOptimize on ; (can be off)
|
||||
CRCCheck on ; (can be off)
|
||||
AutoCloseWindow false ; (can be true for the window go away automatically at end)
|
||||
ShowInstDetails hide ; (can be show to have them shown, or nevershow to disable)
|
||||
SetDateSave off ; (can be on to have files restored to their original date)
|
||||
WindowIcon on
|
||||
SpaceTexts none
|
||||
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Vista/Win7 redirects $SMPROGRAMS to all users without this
|
||||
RequestExecutionLevel admin
|
||||
FileErrorText "If you have no admin rights, try to install into a user directory."
|
||||
|
||||
;------------------------------------------------------------------
|
||||
;Variables
|
||||
Var MUI_TEMP
|
||||
Var STARTMENU_FOLDER
|
||||
Var PREV_INST_DIR
|
||||
|
||||
;------------------------------------------------------------------
|
||||
;Interface Settings
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
;Show all languages, despite user's codepage
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
|
||||
!define MUI_ICON "dist\SABnzbd\icons\sabnzbd.ico"
|
||||
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
|
||||
!insertmacro MUI_PAGE_LICENSE "dist\SABnzbd\LICENSE.txt"
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
;Start Menu Folder Page Configuration
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\SABnzbd"
|
||||
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
|
||||
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "SABnzbd"
|
||||
;Remember the installer language
|
||||
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_LANGDLL_REGISTRY_KEY "Software\SABnzbd"
|
||||
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
|
||||
|
||||
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
|
||||
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun
|
||||
!define MUI_FINISHPAGE_RUN_TEXT $(MsgRunSAB)
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote)
|
||||
!define MUI_FINISHPAGE_LINK $(MsgSupportUs)
|
||||
!define MUI_FINISHPAGE_LINK_LOCATION "https://sabnzbd.org/donate"
|
||||
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!define MUI_UNPAGE_COMPONENTSPAGE_NODESC
|
||||
!insertmacro MUI_UNPAGE_COMPONENTS
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Run as user-level at end of install
|
||||
Function PageFinishRun
|
||||
; Check if SABnzbd service is installed
|
||||
!insertmacro SERVICE "installed" "SABnzbd" ""
|
||||
Pop $0 ;response
|
||||
${If} $0 == true
|
||||
; Service is installed, start the service
|
||||
!insertmacro SERVICE "start" "SABnzbd" ""
|
||||
${Else}
|
||||
; Service not installed, run executable as user
|
||||
${StdUtils.ExecShellAsUser} $0 "$INSTDIR\SABnzbd.exe" "" ""
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Set supported languages
|
||||
;
|
||||
; If you edit this list you also need to edit apireg.py in SABnzbd!
|
||||
;
|
||||
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
|
||||
!insertmacro MUI_LANGUAGE "French"
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
!insertmacro MUI_LANGUAGE "Dutch"
|
||||
!insertmacro MUI_LANGUAGE "Finnish"
|
||||
!insertmacro MUI_LANGUAGE "Polish"
|
||||
!insertmacro MUI_LANGUAGE "Swedish"
|
||||
!insertmacro MUI_LANGUAGE "Danish"
|
||||
!insertmacro MUI_LANGUAGE "Italian"
|
||||
!insertmacro MUI_LANGUAGE "Norwegian"
|
||||
!insertmacro MUI_LANGUAGE "Romanian"
|
||||
!insertmacro MUI_LANGUAGE "Spanish"
|
||||
!insertmacro MUI_LANGUAGE "PortugueseBR"
|
||||
!insertmacro MUI_LANGUAGE "Serbian"
|
||||
!insertmacro MUI_LANGUAGE "Turkish"
|
||||
!insertmacro MUI_LANGUAGE "Hebrew"
|
||||
!insertmacro MUI_LANGUAGE "Russian"
|
||||
!insertmacro MUI_LANGUAGE "Czech"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
|
||||
|
||||
;------------------------------------------------------------------
|
||||
;Reserve Files
|
||||
;If you are using solid compression, files that are required before
|
||||
;the actual installation should be stored first in the data block,
|
||||
;because this will make your installer start faster.
|
||||
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; SECTION main program
|
||||
;
|
||||
Section "SABnzbd" SecDummy
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
SetShellVarContext all
|
||||
|
||||
DetailPrint $(MsgShutting)
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Shutdown any running service
|
||||
|
||||
!insertmacro SERVICE "stop" "SABnzbd" ""
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Terminate SABnzbd.exe
|
||||
loop:
|
||||
${nsProcess::FindProcess} "SABnzbd.exe" $R0
|
||||
StrCmp $R0 0 0 endcheck
|
||||
${nsProcess::CloseProcess} "SABnzbd.exe" $R0
|
||||
Sleep 500
|
||||
Goto loop
|
||||
endcheck:
|
||||
${nsProcess::Unload}
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Make sure old versions are gone (reg-key already read in onInt)
|
||||
StrCmp $PREV_INST_DIR "" noPrevInstallRemove
|
||||
${RemovePrev} "$PREV_INST_DIR"
|
||||
Goto continueSetupAfterRemove
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Add firewall rules for new installs
|
||||
noPrevInstallRemove:
|
||||
liteFirewallW::AddRule "$INSTDIR\SABnzbd.exe" "SABnzbd"
|
||||
liteFirewallW::AddRule "$INSTDIR\SABnzbd-console.exe" "SABnzbd-console"
|
||||
|
||||
continueSetupAfterRemove:
|
||||
|
||||
; add files / whatever that need to be installed here.
|
||||
File /r "dist\SABnzbd\*"
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Add to registry
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "" "$INSTDIR"
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "Installer Language" "$(MsgLangCode)"
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayName" "SABnzbd ${SAB_VERSION}"
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayVersion" '${SAB_VERSION}'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Publisher" 'The SABnzbd-Team'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "HelpLink" 'https://forums.sabnzbd.org/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLInfoAbout" 'https://sabnzbd.org/wiki/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLUpdateInfo" 'https://sabnzbd.org/'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Comments" 'The automated Usenet download tool'
|
||||
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayIcon" '$INSTDIR\icons\sabnzbd.ico'
|
||||
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 40674
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoRepair" -1
|
||||
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoModify" -1
|
||||
|
||||
WriteRegStr HKEY_CURRENT_USER "Software\Classes\AppUserModelId\SABnzbd" "DisplayName" "SABnzbd"
|
||||
WriteRegStr HKEY_CURRENT_USER "Software\Classes\AppUserModelId\SABnzbd" "IconUri" '$INSTDIR\icons\sabnzbd16_32.ico'
|
||||
|
||||
; write out uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||
;Create shortcuts
|
||||
CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - SafeMode.lnk" "$INSTDIR\SABnzbd.exe" "--server 127.0.0.1:8080 -b1 --no-login"
|
||||
WriteINIStr "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - Documentation.url" "InternetShortcut" "URL" "https://sabnzbd.org/wiki/"
|
||||
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
|
||||
!insertmacro MUI_STARTMENU_WRITE_END
|
||||
SectionEnd ; end of default section
|
||||
|
||||
Section $(MsgIcon) desktop
|
||||
CreateShortCut "$DESKTOP\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
|
||||
SectionEnd ; end of desktop icon section
|
||||
|
||||
Section $(MsgAssoc) assoc
|
||||
${registerExtension} "$INSTDIR\icons\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
|
||||
${RefreshShellIcons}
|
||||
SectionEnd ; end of file association section
|
||||
|
||||
Section /o $(MsgRunAtStart) startup
|
||||
CreateShortCut "$SMPROGRAMS\Startup\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" "-b0"
|
||||
SectionEnd ;
|
||||
|
||||
;------------------------------------------------------------------
|
||||
Function .onInit
|
||||
; We need to modify the dir here for X64
|
||||
${If} ${RunningX64}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\SABnzbd"
|
||||
${Else}
|
||||
MessageBox MB_OK|MB_ICONSTOP $(MsgOnly64bit)
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; Python 3.9 no longer supports Windows 7
|
||||
${If} ${AtMostWin8}
|
||||
MessageBox MB_OK|MB_ICONSTOP $(MsgNoWin7)
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Change settings based on if SAB was already installed
|
||||
ReadRegStr $PREV_INST_DIR HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" ""
|
||||
StrCmp $PREV_INST_DIR "" noPrevInstall
|
||||
; We want to use the user's custom dir if he used one
|
||||
StrCmp $PREV_INST_DIR "$PROGRAMFILES\SABnzbd" noSpecialDir
|
||||
StrCmp $PREV_INST_DIR "$PROGRAMFILES64\SABnzbd" noSpecialDir
|
||||
; Set what the user had before
|
||||
StrCpy $INSTDIR "$PREV_INST_DIR"
|
||||
noSpecialDir:
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Check what the user has currently set for install options
|
||||
SetShellVarContext current
|
||||
IfFileExists "$SMPROGRAMS\Startup\SABnzbd.lnk" 0 endCheckStartupCurrent
|
||||
SectionSetFlags ${startup} 1
|
||||
endCheckStartupCurrent:
|
||||
SetShellVarContext all
|
||||
IfFileExists "$SMPROGRAMS\Startup\SABnzbd.lnk" 0 endCheckStartup
|
||||
SectionSetFlags ${startup} 1
|
||||
endCheckStartup:
|
||||
|
||||
SetShellVarContext current
|
||||
IfFileExists "$DESKTOP\SABnzbd.lnk" endCheckDesktop 0
|
||||
; If not present for current user, first check all user folder
|
||||
SetShellVarContext all
|
||||
IfFileExists "$DESKTOP\SABnzbd.lnk" endCheckDesktop 0
|
||||
SectionSetFlags ${desktop} 0 ; SAB is installed but desktop-icon not, so uncheck it
|
||||
endCheckDesktop:
|
||||
SetShellVarContext all
|
||||
|
||||
Push $1
|
||||
ReadRegStr $1 HKCR ".nzb" "" ; read current file association
|
||||
StrCmp "$1" "NZB File" noPrevInstall 0
|
||||
SectionSetFlags ${assoc} 0 ; Uncheck it when it wasn't checked before
|
||||
noPrevInstall:
|
||||
|
||||
;--------------------------------
|
||||
; Display language chooser
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
|
||||
;------------------------------------------------------------------
|
||||
; Tell users about the service change
|
||||
;
|
||||
!insertmacro SERVICE "installed" "SABHelper" ""
|
||||
Pop $0 ;response
|
||||
${If} $0 == true
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgServChange) IDOK removeservices IDCANCEL exitinstall
|
||||
exitinstall:
|
||||
Abort
|
||||
removeservices:
|
||||
!insertmacro SERVICE "delete" "SABHelper" ""
|
||||
!insertmacro SERVICE "delete" "SABnzbd" ""
|
||||
${EndIf}
|
||||
|
||||
FunctionEnd
|
||||
|
||||
;--------------------------------
|
||||
; begin uninstall settings/section
|
||||
UninstallText $(MsgUninstall)
|
||||
|
||||
Section "un.$(MsgDelProgram)" Uninstall
|
||||
;make sure sabnzbd.exe isn't running..if so shut it down
|
||||
DetailPrint $(MsgShutting)
|
||||
${nsProcess::KillProcess} "SABnzbd.exe" $R0
|
||||
${nsProcess::Unload}
|
||||
|
||||
; add delete commands to delete whatever files/registry keys/etc you installed here.
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd"
|
||||
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd"
|
||||
DeleteRegKey HKEY_CURRENT_USER "Software\Classes\AppUserModelId\SABnzbd"
|
||||
DeleteRegKey HKEY_CURRENT_USER "Software\SABnzbd"
|
||||
|
||||
${RemovePrev} "$INSTDIR"
|
||||
${RemovePrevShortcuts}
|
||||
|
||||
; Remove firewall entries
|
||||
liteFirewallW::RemoveRule "$INSTDIR\SABnzbd.exe" "SABnzbd"
|
||||
liteFirewallW::RemoveRule "$INSTDIR\SABnzbd-console.exe" "SABnzbd-console"
|
||||
|
||||
${unregisterExtension} ".nzb" "NZB File"
|
||||
${RefreshShellIcons}
|
||||
|
||||
SectionEnd ; end of uninstall section
|
||||
|
||||
Section /o "un.$(MsgDelSettings)" DelSettings
|
||||
DetailPrint "Uninstall settings $LOCALAPPDATA"
|
||||
Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini"
|
||||
RMDir /r "$LOCALAPPDATA\sabnzbd"
|
||||
SectionEnd
|
||||
|
||||
; eof
|
||||
|
||||
;--------------------------------
|
||||
;Language strings
|
||||
LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes"
|
||||
|
||||
LangString MsgRunSAB ${LANG_ENGLISH} "Run SABnzbd"
|
||||
|
||||
LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!"
|
||||
|
||||
LangString MsgServChange ${LANG_ENGLISH} "The SABnzbd Windows Service changed in SABnzbd 3.0.0. $\nYou will need to reinstall the SABnzbd service. $\n$\nClick `OK` to remove the existing services or `Cancel` to cancel this upgrade."
|
||||
|
||||
LangString MsgOnly64bit ${LANG_ENGLISH} "SABnzbd only supports 64-bit Windows."
|
||||
|
||||
LangString MsgNoWin7 ${LANG_ENGLISH} "SABnzbd only supports Windows 8.1 and above."
|
||||
|
||||
LangString MsgShutting ${LANG_ENGLISH} "Shutting down SABnzbd"
|
||||
|
||||
LangString MsgUninstall ${LANG_ENGLISH} "This will uninstall SABnzbd from your system"
|
||||
|
||||
LangString MsgRunAtStart ${LANG_ENGLISH} "Run at startup"
|
||||
|
||||
LangString MsgIcon ${LANG_ENGLISH} "Desktop Icon"
|
||||
|
||||
LangString MsgAssoc ${LANG_ENGLISH} "NZB File association"
|
||||
|
||||
LangString MsgDelProgram ${LANG_ENGLISH} "Delete Program"
|
||||
|
||||
LangString MsgDelSettings ${LANG_ENGLISH} "Delete Settings"
|
||||
|
||||
LangString MsgLangCode ${LANG_ENGLISH} "en"
|
||||
|
||||
Function un.onInit
|
||||
!insertmacro MUI_UNGETLANGUAGE
|
||||
FunctionEnd
|
||||
@@ -1,501 +0,0 @@
|
||||
#################################################################################
|
||||
# StdUtils plug-in for NSIS
|
||||
# Copyright (C) 2004-2018 LoRd_MuldeR <MuldeR2@GMX.de>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
# http://www.gnu.org/licenses/lgpl-2.1.txt
|
||||
#################################################################################
|
||||
|
||||
# DEVELOPER NOTES:
|
||||
# - Please see "https://github.com/lordmulder/stdutils/" for news and updates!
|
||||
# - Please see "Docs\StdUtils\StdUtils.html" for detailed function descriptions!
|
||||
# - Please see "Examples\StdUtils\StdUtilsTest.nsi" for usage examples!
|
||||
|
||||
#################################################################################
|
||||
# FUNCTION DECLARTIONS
|
||||
#################################################################################
|
||||
|
||||
!ifndef ___STDUTILS__NSH___
|
||||
!define ___STDUTILS__NSH___
|
||||
|
||||
!define StdUtils.Time '!insertmacro _StdU_Time' #time(), as in C standard library
|
||||
!define StdUtils.GetMinutes '!insertmacro _StdU_GetMinutes' #GetSystemTimeAsFileTime(), returns the number of minutes
|
||||
!define StdUtils.GetHours '!insertmacro _StdU_GetHours' #GetSystemTimeAsFileTime(), returns the number of hours
|
||||
!define StdUtils.GetDays '!insertmacro _StdU_GetDays' #GetSystemTimeAsFileTime(), returns the number of days
|
||||
!define StdUtils.Rand '!insertmacro _StdU_Rand' #rand(), as in C standard library
|
||||
!define StdUtils.RandMax '!insertmacro _StdU_RandMax' #rand(), as in C standard library, with maximum value
|
||||
!define StdUtils.RandMinMax '!insertmacro _StdU_RandMinMax' #rand(), as in C standard library, with minimum/maximum value
|
||||
!define StdUtils.RandList '!insertmacro _StdU_RandList' #rand(), as in C standard library, with list support
|
||||
!define StdUtils.RandBytes '!insertmacro _StdU_RandBytes' #Generates random bytes, returned as Base64-encoded string
|
||||
!define StdUtils.FormatStr '!insertmacro _StdU_FormatStr' #sprintf(), as in C standard library, one '%d' placeholder
|
||||
!define StdUtils.FormatStr2 '!insertmacro _StdU_FormatStr2' #sprintf(), as in C standard library, two '%d' placeholders
|
||||
!define StdUtils.FormatStr3 '!insertmacro _StdU_FormatStr3' #sprintf(), as in C standard library, three '%d' placeholders
|
||||
!define StdUtils.ScanStr '!insertmacro _StdU_ScanStr' #sscanf(), as in C standard library, one '%d' placeholder
|
||||
!define StdUtils.ScanStr2 '!insertmacro _StdU_ScanStr2' #sscanf(), as in C standard library, two '%d' placeholders
|
||||
!define StdUtils.ScanStr3 '!insertmacro _StdU_ScanStr3' #sscanf(), as in C standard library, three '%d' placeholders
|
||||
!define StdUtils.TrimStr '!insertmacro _StdU_TrimStr' #Remove whitspaces from string, left and right
|
||||
!define StdUtils.TrimStrLeft '!insertmacro _StdU_TrimStrLeft' #Remove whitspaces from string, left side only
|
||||
!define StdUtils.TrimStrRight '!insertmacro _StdU_TrimStrRight' #Remove whitspaces from string, right side only
|
||||
!define StdUtils.RevStr '!insertmacro _StdU_RevStr' #Reverse a string, e.g. "reverse me" <-> "em esrever"
|
||||
!define StdUtils.ValidFileName '!insertmacro _StdU_ValidFileName' #Test whether string is a valid file name - no paths allowed
|
||||
!define StdUtils.ValidPathSpec '!insertmacro _StdU_ValidPathSpec' #Test whether string is a valid full(!) path specification
|
||||
!define StdUtils.ValidDomainName '!insertmacro _StdU_ValidDomain' #Test whether string is a valid host name or domain name
|
||||
!define StdUtils.StrToUtf8 '!insertmacro _StdU_StrToUtf8' #Convert string from Unicode (UTF-16) or ANSI to UTF-8 bytes
|
||||
!define StdUtils.StrFromUtf8 '!insertmacro _StdU_StrFromUtf8' #Convert string from UTF-8 bytes to Unicode (UTF-16) or ANSI
|
||||
!define StdUtils.SHFileMove '!insertmacro _StdU_SHFileMove' #SHFileOperation(), using the FO_MOVE operation
|
||||
!define StdUtils.SHFileCopy '!insertmacro _StdU_SHFileCopy' #SHFileOperation(), using the FO_COPY operation
|
||||
!define StdUtils.AppendToFile '!insertmacro _StdU_AppendToFile' #Append contents of an existing file to another file
|
||||
!define StdUtils.ExecShellAsUser '!insertmacro _StdU_ExecShlUser' #ShellExecute() as NON-elevated user from elevated installer
|
||||
!define StdUtils.InvokeShellVerb '!insertmacro _StdU_InvkeShlVrb' #Invokes a "shell verb", e.g. for pinning items to the taskbar
|
||||
!define StdUtils.ExecShellWaitEx '!insertmacro _StdU_ExecShlWaitEx' #ShellExecuteEx(), returns the handle of the new process
|
||||
!define StdUtils.WaitForProcEx '!insertmacro _StdU_WaitForProcEx' #WaitForSingleObject(), e.g. to wait for a running process
|
||||
!define StdUtils.GetParameter '!insertmacro _StdU_GetParameter' #Get the value of a specific command-line option
|
||||
!define StdUtils.TestParameter '!insertmacro _StdU_TestParameter' #Test whether a specific command-line option has been set
|
||||
!define StdUtils.ParameterCnt '!insertmacro _StdU_ParameterCnt' #Get number of command-line tokens, similar to argc in main()
|
||||
!define StdUtils.ParameterStr '!insertmacro _StdU_ParameterStr' #Get the n-th command-line token, similar to argv[i] in main()
|
||||
!define StdUtils.GetAllParameters '!insertmacro _StdU_GetAllParams' #Get complete command-line, but without executable name
|
||||
!define StdUtils.GetRealOSVersion '!insertmacro _StdU_GetRealOSVer' #Get the *real* Windows version number, even on Windows 8.1+
|
||||
!define StdUtils.GetRealOSBuildNo '!insertmacro _StdU_GetRealOSBld' #Get the *real* Windows build number, even on Windows 8.1+
|
||||
!define StdUtils.GetRealOSName '!insertmacro _StdU_GetRealOSStr' #Get the *real* Windows version, as a "friendly" name
|
||||
!define StdUtils.GetOSEdition '!insertmacro _StdU_GetOSEdition' #Get the Windows edition, i.e. "workstation" or "server"
|
||||
!define StdUtils.GetOSReleaseId '!insertmacro _StdU_GetOSRelIdNo' #Get the Windows release identifier (on Windows 10)
|
||||
!define StdUtils.GetOSReleaseName '!insertmacro _StdU_GetOSRelIdStr' #Get the Windows release (on Windows 10), as a "friendly" name
|
||||
!define StdUtils.VerifyOSVersion '!insertmacro _StdU_VrfyRealOSVer' #Compare *real* operating system to an expected version number
|
||||
!define StdUtils.VerifyOSBuildNo '!insertmacro _StdU_VrfyRealOSBld' #Compare *real* operating system to an expected build number
|
||||
!define StdUtils.HashText '!insertmacro _StdU_HashText' #Compute hash from text string (CRC32, MD5, SHA1/2/3, BLAKE2)
|
||||
!define StdUtils.HashFile '!insertmacro _StdU_HashFile' #Compute hash from file (CRC32, MD5, SHA1/2/3, BLAKE2)
|
||||
!define StdUtils.NormalizePath '!insertmacro _StdU_NormalizePath' #Simplifies the path to produce a direct, well-formed path
|
||||
!define StdUtils.GetParentPath '!insertmacro _StdU_GetParentPath' #Get parent path by removing the last component from the path
|
||||
!define StdUtils.SplitPath '!insertmacro _StdU_SplitPath' #Split the components of the given path
|
||||
!define StdUtils.GetDrivePart '!insertmacro _StdU_GetDrivePart' #Get drive component of path
|
||||
!define StdUtils.GetDirectoryPart '!insertmacro _StdU_GetDirPart' #Get directory component of path
|
||||
!define StdUtils.GetFileNamePart '!insertmacro _StdU_GetFNamePart' #Get file name component of path
|
||||
!define StdUtils.GetExtensionPart '!insertmacro _StdU_GetExtnPart' #Get file extension component of path
|
||||
!define StdUtils.TimerCreate '!insertmacro _StdU_TimerCreate' #Create a new event-timer that will be triggered periodically
|
||||
!define StdUtils.TimerDestroy '!insertmacro _StdU_TimerDestroy' #Destroy a running timer created with TimerCreate()
|
||||
!define StdUtils.ProtectStr '!insertmacro _StdU_PrtctStr' #Protect a given String using Windows' DPAPI
|
||||
!define StdUtils.UnprotectStr '!insertmacro _StdU_UnprtctStr' #Unprotect a string that was protected via ProtectStr()
|
||||
!define StdUtils.GetLibVersion '!insertmacro _StdU_GetLibVersion' #Get the current StdUtils library version (for debugging)
|
||||
!define StdUtils.SetVerbose '!insertmacro _StdU_SetVerbose' #Enable or disable "verbose" mode (for debugging)
|
||||
|
||||
|
||||
#################################################################################
|
||||
# MACRO DEFINITIONS
|
||||
#################################################################################
|
||||
|
||||
!macro _StdU_Time out
|
||||
StdUtils::Time /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetMinutes out
|
||||
StdUtils::GetMinutes /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetHours out
|
||||
StdUtils::GetHours /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetDays out
|
||||
StdUtils::GetDays /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_Rand out
|
||||
StdUtils::Rand /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_RandMax out max
|
||||
push ${max}
|
||||
StdUtils::RandMax /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_RandMinMax out min max
|
||||
push ${min}
|
||||
push ${max}
|
||||
StdUtils::RandMinMax /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_RandList count max
|
||||
push ${max}
|
||||
push ${count}
|
||||
StdUtils::RandList /NOUNLOAD
|
||||
!macroend
|
||||
|
||||
!macro _StdU_RandBytes out count
|
||||
push ${count}
|
||||
StdUtils::RandBytes /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_FormatStr out format val
|
||||
push `${format}`
|
||||
push ${val}
|
||||
StdUtils::FormatStr /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_FormatStr2 out format val1 val2
|
||||
push `${format}`
|
||||
push ${val1}
|
||||
push ${val2}
|
||||
StdUtils::FormatStr2 /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_FormatStr3 out format val1 val2 val3
|
||||
push `${format}`
|
||||
push ${val1}
|
||||
push ${val2}
|
||||
push ${val3}
|
||||
StdUtils::FormatStr3 /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ScanStr out format input default
|
||||
push `${format}`
|
||||
push `${input}`
|
||||
push ${default}
|
||||
StdUtils::ScanStr /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ScanStr2 out1 out2 format input default1 default2
|
||||
push `${format}`
|
||||
push `${input}`
|
||||
push ${default1}
|
||||
push ${default2}
|
||||
StdUtils::ScanStr2 /NOUNLOAD
|
||||
pop ${out1}
|
||||
pop ${out2}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ScanStr3 out1 out2 out3 format input default1 default2 default3
|
||||
push `${format}`
|
||||
push `${input}`
|
||||
push ${default1}
|
||||
push ${default2}
|
||||
push ${default3}
|
||||
StdUtils::ScanStr3 /NOUNLOAD
|
||||
pop ${out1}
|
||||
pop ${out2}
|
||||
pop ${out3}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TrimStr var
|
||||
push ${var}
|
||||
StdUtils::TrimStr /NOUNLOAD
|
||||
pop ${var}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TrimStrLeft var
|
||||
push ${var}
|
||||
StdUtils::TrimStrLeft /NOUNLOAD
|
||||
pop ${var}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TrimStrRight var
|
||||
push ${var}
|
||||
StdUtils::TrimStrRight /NOUNLOAD
|
||||
pop ${var}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_RevStr var
|
||||
push ${var}
|
||||
StdUtils::RevStr /NOUNLOAD
|
||||
pop ${var}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ValidFileName out test
|
||||
push `${test}`
|
||||
StdUtils::ValidFileName /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ValidPathSpec out test
|
||||
push `${test}`
|
||||
StdUtils::ValidPathSpec /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ValidDomain out test
|
||||
push `${test}`
|
||||
StdUtils::ValidDomainName /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
|
||||
!macro _StdU_StrToUtf8 out str
|
||||
push `${str}`
|
||||
StdUtils::StrToUtf8 /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_StrFromUtf8 out trnc str
|
||||
push ${trnc}
|
||||
push `${str}`
|
||||
StdUtils::StrFromUtf8 /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_SHFileMove out from to hwnd
|
||||
push `${from}`
|
||||
push `${to}`
|
||||
push ${hwnd}
|
||||
StdUtils::SHFileMove /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_SHFileCopy out from to hwnd
|
||||
push `${from}`
|
||||
push `${to}`
|
||||
push ${hwnd}
|
||||
StdUtils::SHFileCopy /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_AppendToFile out from dest offset maxlen
|
||||
push `${from}`
|
||||
push `${dest}`
|
||||
push ${offset}
|
||||
push ${maxlen}
|
||||
StdUtils::AppendToFile /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ExecShlUser out file verb args
|
||||
push `${file}`
|
||||
push `${verb}`
|
||||
push `${args}`
|
||||
StdUtils::ExecShellAsUser /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_InvkeShlVrb out path file verb_id
|
||||
push "${path}"
|
||||
push "${file}"
|
||||
push ${verb_id}
|
||||
StdUtils::InvokeShellVerb /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ExecShlWaitEx out_res out_val file verb args
|
||||
push `${file}`
|
||||
push `${verb}`
|
||||
push `${args}`
|
||||
StdUtils::ExecShellWaitEx /NOUNLOAD
|
||||
pop ${out_res}
|
||||
pop ${out_val}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_WaitForProcEx out handle
|
||||
push `${handle}`
|
||||
StdUtils::WaitForProcEx /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetParameter out name default
|
||||
push `${name}`
|
||||
push `${default}`
|
||||
StdUtils::GetParameter /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TestParameter out name
|
||||
push `${name}`
|
||||
StdUtils::TestParameter /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ParameterCnt out
|
||||
StdUtils::ParameterCnt /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_ParameterStr out index
|
||||
push ${index}
|
||||
StdUtils::ParameterStr /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetAllParams out truncate
|
||||
push `${truncate}`
|
||||
StdUtils::GetAllParameters /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetRealOSVer out_major out_minor out_spack
|
||||
StdUtils::GetRealOsVersion /NOUNLOAD
|
||||
pop ${out_major}
|
||||
pop ${out_minor}
|
||||
pop ${out_spack}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetRealOSBld out
|
||||
StdUtils::GetRealOsBuildNo /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetRealOSStr out
|
||||
StdUtils::GetRealOsName /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_VrfyRealOSVer out major minor spack
|
||||
push `${major}`
|
||||
push `${minor}`
|
||||
push `${spack}`
|
||||
StdUtils::VerifyRealOsVersion /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_VrfyRealOSBld out build
|
||||
push `${build}`
|
||||
StdUtils::VerifyRealOsBuildNo /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetOSEdition out
|
||||
StdUtils::GetOsEdition /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetOSRelIdNo out
|
||||
StdUtils::GetOsReleaseId /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetOSRelIdStr out
|
||||
StdUtils::GetOsReleaseName /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_HashText out type text
|
||||
push `${type}`
|
||||
push `${text}`
|
||||
StdUtils::HashText /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_HashFile out type file
|
||||
push `${type}`
|
||||
push `${file}`
|
||||
StdUtils::HashFile /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_NormalizePath out path
|
||||
push `${path}`
|
||||
StdUtils::NormalizePath /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetParentPath out path
|
||||
push `${path}`
|
||||
StdUtils::GetParentPath /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_SplitPath out_drive out_dir out_fname out_ext path
|
||||
push `${path}`
|
||||
StdUtils::SplitPath /NOUNLOAD
|
||||
pop ${out_drive}
|
||||
pop ${out_dir}
|
||||
pop ${out_fname}
|
||||
pop ${out_ext}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetDrivePart out path
|
||||
push `${path}`
|
||||
StdUtils::GetDrivePart /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetDirPart out path
|
||||
push `${path}`
|
||||
StdUtils::GetDirectoryPart /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetFNamePart out path
|
||||
push `${path}`
|
||||
StdUtils::GetFileNamePart /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetExtnPart out path
|
||||
push `${path}`
|
||||
StdUtils::GetExtensionPart /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TimerCreate out callback interval
|
||||
GetFunctionAddress ${out} ${callback}
|
||||
push ${out}
|
||||
push ${interval}
|
||||
StdUtils::TimerCreate /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_TimerDestroy out timer_id
|
||||
push ${timer_id}
|
||||
StdUtils::TimerDestroy /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_PrtctStr out dpsc salt text
|
||||
push `${dpsc}`
|
||||
push `${salt}`
|
||||
push `${text}`
|
||||
StdUtils::ProtectStr /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_UnprtctStr out trnc salt data
|
||||
push `${trnc}`
|
||||
push `${salt}`
|
||||
push `${data}`
|
||||
StdUtils::UnprotectStr /NOUNLOAD
|
||||
pop ${out}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_GetLibVersion out_ver out_tst
|
||||
StdUtils::GetLibVersion /NOUNLOAD
|
||||
pop ${out_ver}
|
||||
pop ${out_tst}
|
||||
!macroend
|
||||
|
||||
!macro _StdU_SetVerbose enable
|
||||
Push ${enable}
|
||||
StdUtils::SetVerboseMode /NOUNLOAD
|
||||
!macroend
|
||||
|
||||
|
||||
#################################################################################
|
||||
# MAGIC NUMBERS
|
||||
#################################################################################
|
||||
|
||||
!define StdUtils.Const.ShellVerb.PinToTaskbar 0
|
||||
!define StdUtils.Const.ShellVerb.UnpinFromTaskbar 1
|
||||
!define StdUtils.Const.ShellVerb.PinToStart 2
|
||||
!define StdUtils.Const.ShellVerb.UnpinFromStart 3
|
||||
|
||||
!endif # !___STDUTILS__NSH___
|
||||
@@ -1,28 +0,0 @@
|
||||
!define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess`
|
||||
|
||||
!macro nsProcess::FindProcess _FILE _ERR
|
||||
nsProcess::_FindProcess /NOUNLOAD `${_FILE}`
|
||||
Pop ${_ERR}
|
||||
!macroend
|
||||
|
||||
|
||||
!define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess`
|
||||
|
||||
!macro nsProcess::KillProcess _FILE _ERR
|
||||
nsProcess::_KillProcess /NOUNLOAD `${_FILE}`
|
||||
Pop ${_ERR}
|
||||
!macroend
|
||||
|
||||
!define nsProcess::CloseProcess `!insertmacro nsProcess::CloseProcess`
|
||||
|
||||
!macro nsProcess::CloseProcess _FILE _ERR
|
||||
nsProcess::_CloseProcess /NOUNLOAD `${_FILE}`
|
||||
Pop ${_ERR}
|
||||
!macroend
|
||||
|
||||
|
||||
!define nsProcess::Unload `!insertmacro nsProcess::Unload`
|
||||
|
||||
!macro nsProcess::Unload
|
||||
nsProcess::_Unload
|
||||
!macroend
|
||||
@@ -1,411 +0,0 @@
|
||||
; NSIS SERVICE LIBRARY - servicelib.nsh
|
||||
; Version 1.8.1 - Jun 21th, 2013
|
||||
; Questions/Comments - dselkirk@hotmail.com
|
||||
;
|
||||
; Description:
|
||||
; Provides an interface to window services
|
||||
;
|
||||
; Inputs:
|
||||
; action - systemlib action ie. create, delete, start, stop, pause,
|
||||
; continue, installed, running, status
|
||||
; name - name of service to manipulate
|
||||
; param - action parameters; usage: var1=value1;var2=value2;...etc.
|
||||
; (don't forget to add a ';' after the last value!)
|
||||
;
|
||||
; Actions:
|
||||
; create - creates a new windows service
|
||||
; Parameters:
|
||||
; path - path to service executable
|
||||
; autostart - automatically start with system ie. 1|0
|
||||
; interact - interact with the desktop ie. 1|0
|
||||
; depend - service dependencies
|
||||
; user - user that runs the service
|
||||
; password - password of the above user
|
||||
; display - display name in service's console
|
||||
; description - Description of service
|
||||
; starttype - start type (supersedes autostart)
|
||||
; servicetype - service type (supersedes interact)
|
||||
;
|
||||
; delete - deletes a windows service
|
||||
; start - start a stopped windows service
|
||||
; stop - stops a running windows service
|
||||
; pause - pauses a running windows service
|
||||
; continue - continues a paused windows service
|
||||
; installed - is the provided service installed
|
||||
; Parameters:
|
||||
; action - if true then invokes the specified action
|
||||
; running - is the provided service running
|
||||
; Parameters:
|
||||
; action - if true then invokes the specified action
|
||||
; status - check the status of the provided service
|
||||
;
|
||||
; Usage:
|
||||
; Method 1:
|
||||
; Push "action"
|
||||
; Push "name"
|
||||
; Push "param"
|
||||
; Call Service
|
||||
; Pop $0 ;response
|
||||
;
|
||||
; Method 2:
|
||||
; !insertmacro SERVICE "action" "name" "param"
|
||||
;
|
||||
; History:
|
||||
; 1.0 - 09/15/2003 - Initial release
|
||||
; 1.1 - 09/16/2003 - Changed &l to i, thx brainsucker
|
||||
; 1.2 - 02/29/2004 - Fixed documentation.
|
||||
; 1.3 - 01/05/2006 - Fixed interactive flag and pop order (Kichik)
|
||||
; 1.4 - 12/07/2006 - Added display and depend, fixed datatypes (Vitoco)
|
||||
; 1.5 - 06/25/2008 - Added description of service.(DeSafe.com/liuqixing#gmail.com)
|
||||
; 1.5.1 - 06/12/2009 - Added use of __UNINSTALL__
|
||||
; 1.6 - 08/02/2010 - Fixed description implementation (Anders)
|
||||
; 1.7 - 04/11/2010 - Added get running service process id (Nico)
|
||||
; 1.8 - 24/03/2011 - Added starttype and servicetype (Sergius)
|
||||
; 1.8.1 - 21/06/2013 - Added dynamic ASCII & Unicode support (Zinthose)
|
||||
|
||||
!ifndef SERVICELIB
|
||||
!define SERVICELIB
|
||||
|
||||
!define SC_MANAGER_ALL_ACCESS 0x3F
|
||||
!define SC_STATUS_PROCESS_INFO 0x0
|
||||
!define SERVICE_ALL_ACCESS 0xF01FF
|
||||
|
||||
!define SERVICE_CONTROL_STOP 1
|
||||
!define SERVICE_CONTROL_PAUSE 2
|
||||
!define SERVICE_CONTROL_CONTINUE 3
|
||||
|
||||
!define SERVICE_STOPPED 0x1
|
||||
!define SERVICE_START_PENDING 0x2
|
||||
!define SERVICE_STOP_PENDING 0x3
|
||||
!define SERVICE_RUNNING 0x4
|
||||
!define SERVICE_CONTINUE_PENDING 0x5
|
||||
!define SERVICE_PAUSE_PENDING 0x6
|
||||
!define SERVICE_PAUSED 0x7
|
||||
|
||||
!define SERVICE_KERNEL_DRIVER 0x00000001
|
||||
!define SERVICE_FILE_SYSTEM_DRIVER 0x00000002
|
||||
!define SERVICE_WIN32_OWN_PROCESS 0x00000010
|
||||
!define SERVICE_WIN32_SHARE_PROCESS 0x00000020
|
||||
!define SERVICE_INTERACTIVE_PROCESS 0x00000100
|
||||
|
||||
|
||||
!define SERVICE_BOOT_START 0x00000000
|
||||
!define SERVICE_SYSTEM_START 0x00000001
|
||||
!define SERVICE_AUTO_START 0x00000002
|
||||
!define SERVICE_DEMAND_START 0x00000003
|
||||
!define SERVICE_DISABLED 0x00000004
|
||||
|
||||
## Added by Zinthose for Native Unicode Support
|
||||
!ifdef NSIS_UNICODE
|
||||
!define APITAG "W"
|
||||
!else
|
||||
!define APITAG "A"
|
||||
!endif
|
||||
|
||||
!macro SERVICE ACTION NAME PARAM
|
||||
Push '${ACTION}'
|
||||
Push '${NAME}'
|
||||
Push '${PARAM}'
|
||||
!ifdef __UNINSTALL__
|
||||
Call un.Service
|
||||
!else
|
||||
Call Service
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro FUNC_GETPARAM
|
||||
Push $0
|
||||
Push $1
|
||||
Push $2
|
||||
Push $3
|
||||
Push $4
|
||||
Push $5
|
||||
Push $6
|
||||
Push $7
|
||||
Exch 8
|
||||
Pop $1 ;name
|
||||
Exch 8
|
||||
Pop $2 ;source
|
||||
StrCpy $0 ""
|
||||
StrLen $7 $2
|
||||
StrCpy $3 0
|
||||
lbl_loop:
|
||||
IntCmp $3 $7 0 0 lbl_done
|
||||
StrLen $4 "$1="
|
||||
StrCpy $5 $2 $4 $3
|
||||
StrCmp $5 "$1=" 0 lbl_next
|
||||
IntOp $5 $3 + $4
|
||||
StrCpy $3 $5
|
||||
lbl_loop2:
|
||||
IntCmp $3 $7 0 0 lbl_done
|
||||
StrCpy $6 $2 1 $3
|
||||
StrCmp $6 ";" 0 lbl_next2
|
||||
IntOp $6 $3 - $5
|
||||
StrCpy $0 $2 $6 $5
|
||||
Goto lbl_done
|
||||
lbl_next2:
|
||||
IntOp $3 $3 + 1
|
||||
Goto lbl_loop2
|
||||
lbl_next:
|
||||
IntOp $3 $3 + 1
|
||||
Goto lbl_loop
|
||||
lbl_done:
|
||||
Pop $5
|
||||
Pop $4
|
||||
Pop $3
|
||||
Pop $2
|
||||
Pop $1
|
||||
Exch 2
|
||||
Pop $6
|
||||
Pop $7
|
||||
Exch $0
|
||||
!macroend
|
||||
|
||||
!macro CALL_GETPARAM VAR NAME DEFAULT LABEL
|
||||
Push $1
|
||||
Push ${NAME}
|
||||
Call ${UN}GETPARAM
|
||||
Pop $6
|
||||
StrCpy ${VAR} "${DEFAULT}"
|
||||
StrCmp $6 "" "${LABEL}" 0
|
||||
StrCpy ${VAR} $6
|
||||
!macroend
|
||||
|
||||
!macro FUNC_SERVICE UN
|
||||
Push $0
|
||||
Push $1
|
||||
Push $2
|
||||
Push $3
|
||||
Push $4
|
||||
Push $5
|
||||
Push $6
|
||||
Push $7
|
||||
Exch 8
|
||||
Pop $1 ;param
|
||||
Exch 8
|
||||
Pop $2 ;name
|
||||
Exch 8
|
||||
Pop $3 ;action
|
||||
;$0 return
|
||||
;$4 OpenSCManager
|
||||
;$5 OpenService
|
||||
|
||||
StrCpy $0 "false"
|
||||
System::Call 'advapi32::OpenSCManager${APITAG}(n, n, i ${SC_MANAGER_ALL_ACCESS}) i.r4'
|
||||
IntCmp $4 0 lbl_done
|
||||
StrCmp $3 "create" lbl_create
|
||||
System::Call 'advapi32::OpenService${APITAG}(i r4, t r2, i ${SERVICE_ALL_ACCESS}) i.r5'
|
||||
IntCmp $5 0 lbl_done
|
||||
|
||||
lbl_select:
|
||||
StrCmp $3 "delete" lbl_delete
|
||||
StrCmp $3 "start" lbl_start
|
||||
StrCmp $3 "stop" lbl_stop
|
||||
StrCmp $3 "pause" lbl_pause
|
||||
StrCmp $3 "continue" lbl_continue
|
||||
StrCmp $3 "installed" lbl_installed
|
||||
StrCmp $3 "running" lbl_running
|
||||
StrCmp $3 "status" lbl_status
|
||||
StrCmp $3 "processid" lbl_processid
|
||||
Goto lbl_done
|
||||
|
||||
; create service
|
||||
lbl_create:
|
||||
Push $R1 ;depend
|
||||
Push $R2 ;user
|
||||
Push $R3 ;password
|
||||
Push $R4 ;servicetype/interact
|
||||
Push $R5 ;starttype/autostart
|
||||
Push $R6 ;path
|
||||
Push $R7 ;display
|
||||
Push $R8 ;description
|
||||
|
||||
!insertmacro CALL_GETPARAM $R1 "depend" "n" "lbl_depend"
|
||||
StrCpy $R1 't "$R1"'
|
||||
lbl_depend:
|
||||
StrCmp $R1 "n" 0 lbl_machine ;old name of depend param
|
||||
!insertmacro CALL_GETPARAM $R1 "machine" "n" "lbl_machine"
|
||||
StrCpy $R1 't "$R1"'
|
||||
lbl_machine:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R2 "user" "n" "lbl_user"
|
||||
StrCpy $R2 't "$R2"'
|
||||
lbl_user:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R3 "password" "n" "lbl_password"
|
||||
StrCpy $R3 't "$R3"'
|
||||
lbl_password:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R4 "interact" "${SERVICE_WIN32_OWN_PROCESS}" "lbl_interact"
|
||||
StrCpy $6 ${SERVICE_WIN32_OWN_PROCESS}
|
||||
IntCmp $R4 0 +2
|
||||
IntOp $6 $6 | ${SERVICE_INTERACTIVE_PROCESS}
|
||||
StrCpy $R4 $6
|
||||
lbl_interact:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R4 "servicetype" "$R4" "lbl_servicetype"
|
||||
lbl_servicetype:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R5 "autostart" "${SERVICE_DEMAND_START}" "lbl_autostart"
|
||||
StrCpy $6 ${SERVICE_DEMAND_START}
|
||||
IntCmp $R5 0 +2
|
||||
StrCpy $6 ${SERVICE_AUTO_START}
|
||||
StrCpy $R5 $6
|
||||
lbl_autostart:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R5 "starttype" "$R5" "lbl_starttype"
|
||||
lbl_starttype:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R6 "path" "n" "lbl_path"
|
||||
lbl_path:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R7 "display" "$2" "lbl_display"
|
||||
lbl_display:
|
||||
|
||||
!insertmacro CALL_GETPARAM $R8 "description" "$2" "lbl_description"
|
||||
lbl_description:
|
||||
|
||||
System::Call 'advapi32::CreateService${APITAG}(i r4, t r2, t R7, i ${SERVICE_ALL_ACCESS}, \
|
||||
i R4, i R5, i 0, t R6, n, n, $R1, $R2, $R3) i.r6'
|
||||
|
||||
; write description of service (SERVICE_CONFIG_DESCRIPTION)
|
||||
System::Call 'advapi32::ChangeServiceConfig2${APITAG}(ir6,i1,*t "$R8")i.R7'
|
||||
strcmp $R7 "error" 0 lbl_descriptioncomplete
|
||||
WriteRegStr HKLM "SYSTEM\CurrentControlSet\Services\$2" "Description" $R8
|
||||
lbl_descriptioncomplete:
|
||||
|
||||
Pop $R8
|
||||
Pop $R7
|
||||
Pop $R6
|
||||
Pop $R5
|
||||
Pop $R4
|
||||
Pop $R3
|
||||
Pop $R2
|
||||
Pop $R1
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; delete service
|
||||
lbl_delete:
|
||||
System::Call 'advapi32::DeleteService(i r5) i.r6'
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; start service
|
||||
lbl_start:
|
||||
System::Call 'advapi32::StartService${APITAG}(i r5, i 0, i 0) i.r6'
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; stop service
|
||||
lbl_stop:
|
||||
Push $R1
|
||||
System::Call '*(i,i,i,i,i,i,i) i.R1'
|
||||
System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_STOP}, i $R1) i'
|
||||
System::Free $R1
|
||||
Pop $R1
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; pause service
|
||||
lbl_pause:
|
||||
Push $R1
|
||||
System::Call '*(i,i,i,i,i,i,i) i.R1'
|
||||
System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_PAUSE}, i $R1) i'
|
||||
System::Free $R1
|
||||
Pop $R1
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; continue service
|
||||
lbl_continue:
|
||||
Push $R1
|
||||
System::Call '*(i,i,i,i,i,i,i) i.R1'
|
||||
System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_CONTINUE}, i $R1) i'
|
||||
System::Free $R1
|
||||
Pop $R1
|
||||
StrCmp $6 0 lbl_done lbl_good
|
||||
|
||||
; is installed
|
||||
lbl_installed:
|
||||
!insertmacro CALL_GETPARAM $7 "action" "" "lbl_good"
|
||||
StrCpy $3 $7
|
||||
Goto lbl_select
|
||||
|
||||
; is service running
|
||||
lbl_running:
|
||||
Push $R1
|
||||
System::Call '*(i,i,i,i,i,i,i) i.R1'
|
||||
System::Call 'advapi32::QueryServiceStatus(i r5, i $R1) i'
|
||||
System::Call '*$R1(i, i.r6)'
|
||||
System::Free $R1
|
||||
Pop $R1
|
||||
IntFmt $6 "0x%X" $6
|
||||
StrCmp $6 ${SERVICE_RUNNING} 0 lbl_done
|
||||
!insertmacro CALL_GETPARAM $7 "action" "" "lbl_good"
|
||||
StrCpy $3 $7
|
||||
Goto lbl_select
|
||||
|
||||
lbl_status:
|
||||
Push $R1
|
||||
System::Call '*(i,i,i,i,i,i,i) i.R1'
|
||||
System::Call 'advapi32::QueryServiceStatus(i r5, i $R1) i'
|
||||
System::Call '*$R1(i, i .r6)'
|
||||
System::Free $R1
|
||||
Pop $R1
|
||||
IntFmt $6 "0x%X" $6
|
||||
StrCpy $0 "running"
|
||||
IntCmp $6 ${SERVICE_RUNNING} lbl_done
|
||||
StrCpy $0 "stopped"
|
||||
IntCmp $6 ${SERVICE_STOPPED} lbl_done
|
||||
StrCpy $0 "start_pending"
|
||||
IntCmp $6 ${SERVICE_START_PENDING} lbl_done
|
||||
StrCpy $0 "stop_pending"
|
||||
IntCmp $6 ${SERVICE_STOP_PENDING} lbl_done
|
||||
StrCpy $0 "running"
|
||||
IntCmp $6 ${SERVICE_RUNNING} lbl_done
|
||||
StrCpy $0 "continue_pending"
|
||||
IntCmp $6 ${SERVICE_CONTINUE_PENDING} lbl_done
|
||||
StrCpy $0 "pause_pending"
|
||||
IntCmp $6 ${SERVICE_PAUSE_PENDING} lbl_done
|
||||
StrCpy $0 "paused"
|
||||
IntCmp $6 ${SERVICE_PAUSED} lbl_done
|
||||
StrCpy $0 "unknown"
|
||||
Goto lbl_done
|
||||
|
||||
lbl_processid:
|
||||
Push $R1
|
||||
Push $R2
|
||||
System::Call '*(i,i,i,i,i,i,i,i,i) i.R1'
|
||||
System::Call '*(i 0) i.R2'
|
||||
System::Call "advapi32::QueryServiceStatusEx(i r5, i ${SC_STATUS_PROCESS_INFO}, i $R1, i 36, i $R2) i"
|
||||
System::Call "*$R1(i,i,i,i,i,i,i, i .r0)"
|
||||
System::Free $R2
|
||||
System::Free $R1
|
||||
Pop $R2
|
||||
Pop $R1
|
||||
Goto lbl_done
|
||||
|
||||
lbl_good:
|
||||
StrCpy $0 "true"
|
||||
lbl_done:
|
||||
IntCmp $5 0 +2
|
||||
System::Call 'advapi32::CloseServiceHandle(i r5) n'
|
||||
IntCmp $4 0 +2
|
||||
System::Call 'advapi32::CloseServiceHandle(i r4) n'
|
||||
Pop $4
|
||||
Pop $3
|
||||
Pop $2
|
||||
Pop $1
|
||||
Exch 3
|
||||
Pop $5
|
||||
Pop $7
|
||||
Pop $6
|
||||
Exch $0
|
||||
!macroend
|
||||
|
||||
Function Service
|
||||
!insertmacro FUNC_SERVICE ""
|
||||
FunctionEnd
|
||||
|
||||
Function GetParam
|
||||
!insertmacro FUNC_GETPARAM
|
||||
FunctionEnd
|
||||
|
||||
!undef APITAG
|
||||
!endif
|
||||
25
cherrypy/LICENSE.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2004-2007, CherryPy Team (team@cherrypy.org)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the CherryPy Team nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1
cherrypy/VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
This CherryPy Rev 2138 patched with Rev 2272.
|
||||
561
cherrypy/__init__.py
Normal file
@@ -0,0 +1,561 @@
|
||||
"""CherryPy is a pythonic, object-oriented HTTP framework.
|
||||
|
||||
|
||||
CherryPy consists of not one, but four separate API layers.
|
||||
|
||||
The APPLICATION LAYER is the simplest. CherryPy applications are written as
|
||||
a tree of classes and methods, where each branch in the tree corresponds to
|
||||
a branch in the URL path. Each method is a 'page handler', which receives
|
||||
GET and POST params as keyword arguments, and returns or yields the (HTML)
|
||||
body of the response. The special method name 'index' is used for paths
|
||||
that end in a slash, and the special method name 'default' is used to
|
||||
handle multiple paths via a single handler. This layer also includes:
|
||||
|
||||
* the 'exposed' attribute (and cherrypy.expose)
|
||||
* cherrypy.quickstart()
|
||||
* _cp_config attributes
|
||||
* cherrypy.tools (including cherrypy.session)
|
||||
* cherrypy.url()
|
||||
|
||||
The ENVIRONMENT LAYER is used by developers at all levels. It provides
|
||||
information about the current request and response, plus the application
|
||||
and server environment, via a (default) set of top-level objects:
|
||||
|
||||
* cherrypy.request
|
||||
* cherrypy.response
|
||||
* cherrypy.engine
|
||||
* cherrypy.server
|
||||
* cherrypy.tree
|
||||
* cherrypy.config
|
||||
* cherrypy.thread_data
|
||||
* cherrypy.log
|
||||
* cherrypy.HTTPError, NotFound, and HTTPRedirect
|
||||
* cherrypy.lib
|
||||
|
||||
The EXTENSION LAYER allows advanced users to construct and share their own
|
||||
plugins. It consists of:
|
||||
|
||||
* Hook API
|
||||
* Tool API
|
||||
* Toolbox API
|
||||
* Dispatch API
|
||||
* Config Namespace API
|
||||
|
||||
Finally, there is the CORE LAYER, which uses the core API's to construct
|
||||
the default components which are available at higher layers. You can think
|
||||
of the default components as the 'reference implementation' for CherryPy.
|
||||
Megaframeworks (and advanced users) may replace the default components
|
||||
with customized or extended components. The core API's are:
|
||||
|
||||
* Application API
|
||||
* Engine API
|
||||
* Request API
|
||||
* Server API
|
||||
* WSGI API
|
||||
|
||||
These API's are described in the CherryPy specification:
|
||||
http://www.cherrypy.org/wiki/CherryPySpec
|
||||
"""
|
||||
|
||||
__version__ = "3.2.0"
|
||||
|
||||
from urlparse import urljoin as _urljoin
|
||||
|
||||
|
||||
class _AttributeDocstrings(type):
|
||||
"""Metaclass for declaring docstrings for class attributes."""
|
||||
# The full docstring for this type is down in the __init__ method so
|
||||
# that it doesn't show up in help() for every consumer class.
|
||||
|
||||
def __init__(cls, name, bases, dct):
|
||||
'''Metaclass for declaring docstrings for class attributes.
|
||||
|
||||
Base Python doesn't provide any syntax for setting docstrings on
|
||||
'data attributes' (non-callables). This metaclass allows class
|
||||
definitions to follow the declaration of a data attribute with
|
||||
a docstring for that attribute; the attribute docstring will be
|
||||
popped from the class dict and folded into the class docstring.
|
||||
|
||||
The naming convention for attribute docstrings is:
|
||||
<attrname> + "__doc".
|
||||
For example:
|
||||
|
||||
class Thing(object):
|
||||
"""A thing and its properties."""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
height = 50
|
||||
height__doc = """The height of the Thing in inches."""
|
||||
|
||||
In which case, help(Thing) starts like this:
|
||||
|
||||
>>> help(mod.Thing)
|
||||
Help on class Thing in module pkg.mod:
|
||||
|
||||
class Thing(__builtin__.object)
|
||||
| A thing and its properties.
|
||||
|
|
||||
| height [= 50]:
|
||||
| The height of the Thing in inches.
|
||||
|
|
||||
|
||||
The benefits of this approach over hand-edited class docstrings:
|
||||
1. Places the docstring nearer to the attribute declaration.
|
||||
2. Makes attribute docs more uniform ("name (default): doc").
|
||||
3. Reduces mismatches of attribute _names_ between
|
||||
the declaration and the documentation.
|
||||
4. Reduces mismatches of attribute default _values_ between
|
||||
the declaration and the documentation.
|
||||
|
||||
The benefits of a metaclass approach over other approaches:
|
||||
1. Simpler ("less magic") than interface-based solutions.
|
||||
2. __metaclass__ can be specified at the module global level
|
||||
for classic classes.
|
||||
|
||||
For various formatting reasons, you should write multiline docs
|
||||
with a leading newline and not a trailing one:
|
||||
|
||||
response__doc = """
|
||||
The response object for the current thread. In the main thread,
|
||||
and any threads which are not HTTP requests, this is None."""
|
||||
|
||||
The type of the attribute is intentionally not included, because
|
||||
that's not How Python Works. Quack.
|
||||
'''
|
||||
|
||||
newdoc = [cls.__doc__ or ""]
|
||||
|
||||
dctnames = dct.keys()
|
||||
dctnames.sort()
|
||||
|
||||
for name in dctnames:
|
||||
if name.endswith("__doc"):
|
||||
# Remove the magic doc attribute.
|
||||
if hasattr(cls, name):
|
||||
delattr(cls, name)
|
||||
|
||||
# Make a uniformly-indented docstring from it.
|
||||
val = '\n'.join([' ' + line.strip()
|
||||
for line in dct[name].split('\n')])
|
||||
|
||||
# Get the default value.
|
||||
attrname = name[:-5]
|
||||
try:
|
||||
attrval = getattr(cls, attrname)
|
||||
except AttributeError:
|
||||
attrval = "missing"
|
||||
|
||||
# Add the complete attribute docstring to our list.
|
||||
newdoc.append("%s [= %r]:\n%s" % (attrname, attrval, val))
|
||||
|
||||
# Add our list of new docstrings to the class docstring.
|
||||
cls.__doc__ = "\n\n".join(newdoc)
|
||||
|
||||
|
||||
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
|
||||
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
|
||||
|
||||
from cherrypy import _cpdispatch as dispatch
|
||||
|
||||
from cherrypy import _cptools
|
||||
tools = _cptools.default_toolbox
|
||||
Tool = _cptools.Tool
|
||||
|
||||
from cherrypy import _cprequest
|
||||
from cherrypy.lib import http as _http
|
||||
|
||||
from cherrypy import _cptree
|
||||
tree = _cptree.Tree()
|
||||
from cherrypy._cptree import Application
|
||||
from cherrypy import _cpwsgi as wsgi
|
||||
|
||||
from cherrypy import process
|
||||
try:
|
||||
from cherrypy.process import win32
|
||||
engine = win32.Win32Bus()
|
||||
engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
|
||||
del win32
|
||||
except ImportError:
|
||||
engine = process.bus
|
||||
|
||||
|
||||
# Timeout monitor
|
||||
class _TimeoutMonitor(process.plugins.Monitor):
|
||||
|
||||
def __init__(self, bus):
|
||||
self.servings = []
|
||||
process.plugins.Monitor.__init__(self, bus, self.run)
|
||||
|
||||
def acquire(self):
|
||||
self.servings.append((serving.request, serving.response))
|
||||
|
||||
def release(self):
|
||||
try:
|
||||
self.servings.remove((serving.request, serving.response))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""Check timeout on all responses. (Internal)"""
|
||||
for req, resp in self.servings:
|
||||
resp.check_timeout()
|
||||
engine.timeout_monitor = _TimeoutMonitor(engine)
|
||||
engine.timeout_monitor.subscribe()
|
||||
|
||||
engine.autoreload = process.plugins.Autoreloader(engine)
|
||||
engine.autoreload.subscribe()
|
||||
|
||||
engine.thread_manager = process.plugins.ThreadManager(engine)
|
||||
engine.thread_manager.subscribe()
|
||||
|
||||
engine.signal_handler = process.plugins.SignalHandler(engine)
|
||||
|
||||
|
||||
from cherrypy import _cpserver
|
||||
server = _cpserver.Server()
|
||||
server.subscribe()
|
||||
|
||||
|
||||
def quickstart(root=None, script_name="", config=None):
|
||||
"""Mount the given root, start the builtin server (and engine), then block.
|
||||
|
||||
root: an instance of a "controller class" (a collection of page handler
|
||||
methods) which represents the root of the application.
|
||||
script_name: a string containing the "mount point" of the application.
|
||||
This should start with a slash, and be the path portion of the URL
|
||||
at which to mount the given root. For example, if root.index() will
|
||||
handle requests to "http://www.example.com:8080/dept/app1/", then
|
||||
the script_name argument would be "/dept/app1".
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the root
|
||||
of the URI, it MUST be an empty string (not "/").
|
||||
config: a file or dict containing application config. If this contains
|
||||
a [global] section, those entries will be used in the global
|
||||
(site-wide) config.
|
||||
"""
|
||||
if config:
|
||||
_global_conf_alias.update(config)
|
||||
|
||||
if root is not None:
|
||||
tree.mount(root, script_name, config)
|
||||
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, "console_control_handler"):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
engine.start()
|
||||
engine.block()
|
||||
|
||||
|
||||
try:
|
||||
from threading import local as _local
|
||||
except ImportError:
|
||||
from cherrypy._cpthreadinglocal import local as _local
|
||||
|
||||
class _Serving(_local):
|
||||
"""An interface for registering request and response objects.
|
||||
|
||||
Rather than have a separate "thread local" object for the request and
|
||||
the response, this class works as a single threadlocal container for
|
||||
both objects (and any others which developers wish to define). In this
|
||||
way, we can easily dump those objects when we stop/start a new HTTP
|
||||
conversation, yet still refer to them as module-level globals in a
|
||||
thread-safe way.
|
||||
"""
|
||||
|
||||
__metaclass__ = _AttributeDocstrings
|
||||
|
||||
request = _cprequest.Request(_http.Host("127.0.0.1", 80),
|
||||
_http.Host("127.0.0.1", 1111))
|
||||
request__doc = """
|
||||
The request object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
response = _cprequest.Response()
|
||||
response__doc = """
|
||||
The response object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
def load(self, request, response):
|
||||
self.request = request
|
||||
self.response = response
|
||||
|
||||
def clear(self):
|
||||
"""Remove all attributes of self."""
|
||||
self.__dict__.clear()
|
||||
|
||||
serving = _Serving()
|
||||
|
||||
|
||||
class _ThreadLocalProxy(object):
|
||||
|
||||
__slots__ = ['__attrname__', '__dict__']
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.__attrname__ = attrname
|
||||
|
||||
def __getattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return getattr(child, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ("__attrname__", ):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
child = getattr(serving, self.__attrname__)
|
||||
setattr(child, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
delattr(child, name)
|
||||
|
||||
def _get_dict(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
d = child.__class__.__dict__.copy()
|
||||
d.update(child.__dict__)
|
||||
return d
|
||||
__dict__ = property(_get_dict)
|
||||
|
||||
def __getitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return child[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
child[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
del child[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return key in child
|
||||
|
||||
def __len__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return len(child)
|
||||
|
||||
def __nonzero__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return bool(child)
|
||||
|
||||
|
||||
# Create request and response object (the same objects will be used
|
||||
# throughout the entire life of the webserver, but will redirect
|
||||
# to the "serving" object)
|
||||
request = _ThreadLocalProxy('request')
|
||||
response = _ThreadLocalProxy('response')
|
||||
|
||||
# Create thread_data object as a thread-specific all-purpose storage
|
||||
class _ThreadData(_local):
|
||||
"""A container for thread-specific data."""
|
||||
thread_data = _ThreadData()
|
||||
|
||||
|
||||
# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
|
||||
# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
|
||||
# The only other way would be to change what is returned from type(request)
|
||||
# and that's not possible in pure Python (you'd have to fake ob_type).
|
||||
def _cherrypy_pydoc_resolve(thing, forceload=0):
|
||||
"""Given an object or a path to an object, get the object and its name."""
|
||||
if isinstance(thing, _ThreadLocalProxy):
|
||||
thing = getattr(serving, thing.__attrname__)
|
||||
return _pydoc._builtin_resolve(thing, forceload)
|
||||
|
||||
try:
|
||||
import pydoc as _pydoc
|
||||
_pydoc._builtin_resolve = _pydoc.resolve
|
||||
_pydoc.resolve = _cherrypy_pydoc_resolve
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
from cherrypy import _cplogging
|
||||
|
||||
class _GlobalLogManager(_cplogging.LogManager):
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
log = request.app.log
|
||||
except AttributeError:
|
||||
log = self
|
||||
return log.error(*args, **kwargs)
|
||||
|
||||
def access(self):
|
||||
try:
|
||||
return request.app.log.access()
|
||||
except AttributeError:
|
||||
return _cplogging.LogManager.access(self)
|
||||
|
||||
|
||||
log = _GlobalLogManager()
|
||||
# Set a default screen handler on the global log.
|
||||
log.screen = True
|
||||
log.error_file = ''
|
||||
# Using an access file makes CP about 10% slower. Leave off by default.
|
||||
log.access_file = ''
|
||||
|
||||
def _buslog(msg, level):
|
||||
log.error(msg, 'ENGINE', severity=level)
|
||||
engine.subscribe('log', _buslog)
|
||||
|
||||
# Helper functions for CP apps #
|
||||
|
||||
|
||||
def expose(func=None, alias=None):
|
||||
"""Expose the function, optionally providing an alias or set of aliases."""
|
||||
def expose_(func):
|
||||
func.exposed = True
|
||||
if alias is not None:
|
||||
if isinstance(alias, basestring):
|
||||
parents[alias.replace(".", "_")] = func
|
||||
else:
|
||||
for a in alias:
|
||||
parents[a.replace(".", "_")] = func
|
||||
return func
|
||||
|
||||
import sys, types
|
||||
if isinstance(func, (types.FunctionType, types.MethodType)):
|
||||
if alias is None:
|
||||
# @expose
|
||||
func.exposed = True
|
||||
return func
|
||||
else:
|
||||
# func = expose(func, alias)
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_(func)
|
||||
elif func is None:
|
||||
if alias is None:
|
||||
# @expose()
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_
|
||||
else:
|
||||
# @expose(alias="alias") or
|
||||
# @expose(alias=["alias1", "alias2"])
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_
|
||||
else:
|
||||
# @expose("alias") or
|
||||
# @expose(["alias1", "alias2"])
|
||||
parents = sys._getframe(1).f_locals
|
||||
alias = func
|
||||
return expose_
|
||||
|
||||
|
||||
def url(path="", qs="", script_name=None, base=None, relative=None):
|
||||
"""Create an absolute URL for the given path.
|
||||
|
||||
If 'path' starts with a slash ('/'), this will return
|
||||
(base + script_name + path + qs).
|
||||
If it does not start with a slash, this returns
|
||||
(base + script_name [+ request.path_info] + path + qs).
|
||||
|
||||
If script_name is None, cherrypy.request will be used
|
||||
to find a script_name, if available.
|
||||
|
||||
If base is None, cherrypy.request.base will be used (if available).
|
||||
Note that you can use cherrypy.tools.proxy to change this.
|
||||
|
||||
Finally, note that this function can be used to obtain an absolute URL
|
||||
for the current request path (minus the querystring) by passing no args.
|
||||
If you call url(qs=cherrypy.request.query_string), you should get the
|
||||
original browser URL (assuming no internal redirections).
|
||||
|
||||
If relative is None or not provided, request.app.relative_urls will
|
||||
be used (if available, else False). If False, the output will be an
|
||||
absolute URL (including the scheme, host, vhost, and script_name).
|
||||
If True, the output will instead be a URL that is relative to the
|
||||
current request path, perhaps including '..' atoms. If relative is
|
||||
the string 'server', the output will instead be a URL that is
|
||||
relative to the server root; i.e., it will start with a slash.
|
||||
"""
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
|
||||
if request.app:
|
||||
if not path.startswith("/"):
|
||||
# Append/remove trailing slash from path_info as needed
|
||||
# (this is to support mistyped URL's without redirecting;
|
||||
# if you want to redirect, use tools.trailing_slash).
|
||||
pi = request.path_info
|
||||
if request.is_index is True:
|
||||
if not pi.endswith('/'):
|
||||
pi = pi + '/'
|
||||
elif request.is_index is False:
|
||||
if pi.endswith('/') and pi != '/':
|
||||
pi = pi[:-1]
|
||||
|
||||
if path == "":
|
||||
path = pi
|
||||
else:
|
||||
path = _urljoin(pi, path)
|
||||
|
||||
if script_name is None:
|
||||
script_name = request.script_name
|
||||
if base is None:
|
||||
base = request.base
|
||||
|
||||
newurl = base + script_name + path + qs
|
||||
else:
|
||||
# No request.app (we're being called outside a request).
|
||||
# We'll have to guess the base from server.* attributes.
|
||||
# This will produce very different results from the above
|
||||
# if you're using vhosts or tools.proxy.
|
||||
if base is None:
|
||||
base = server.base()
|
||||
|
||||
path = (script_name or "") + path
|
||||
newurl = base + path + qs
|
||||
|
||||
if './' in newurl:
|
||||
# Normalize the URL by removing ./ and ../
|
||||
atoms = []
|
||||
for atom in newurl.split('/'):
|
||||
if atom == '.':
|
||||
pass
|
||||
elif atom == '..':
|
||||
atoms.pop()
|
||||
else:
|
||||
atoms.append(atom)
|
||||
newurl = '/'.join(atoms)
|
||||
|
||||
# At this point, we should have a fully-qualified absolute URL.
|
||||
|
||||
if relative is None:
|
||||
relative = getattr(request.app, "relative_urls", False)
|
||||
|
||||
# See http://www.ietf.org/rfc/rfc2396.txt
|
||||
if relative == 'server':
|
||||
# "A relative reference beginning with a single slash character is
|
||||
# termed an absolute-path reference, as defined by <abs_path>..."
|
||||
# This is also sometimes called "server-relative".
|
||||
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
|
||||
elif relative:
|
||||
# "A relative reference that does not begin with a scheme name
|
||||
# or a slash character is termed a relative-path reference."
|
||||
old = url().split('/')[:-1]
|
||||
new = newurl.split('/')
|
||||
while old and new:
|
||||
a, b = old[0], new[0]
|
||||
if a != b:
|
||||
break
|
||||
old.pop(0)
|
||||
new.pop(0)
|
||||
new = (['..'] * len(old)) + new
|
||||
newurl = '/'.join(new)
|
||||
|
||||
return newurl
|
||||
|
||||
|
||||
# import _cpconfig last so it can reference other top-level objects
|
||||
from cherrypy import _cpconfig
|
||||
# Use _global_conf_alias so quickstart can use 'config' as an arg
|
||||
# without shadowing cherrypy.config.
|
||||
config = _global_conf_alias = _cpconfig.Config()
|
||||
|
||||
from cherrypy import _cpchecker
|
||||
checker = _cpchecker.Checker()
|
||||
engine.subscribe('start', checker)
|
||||
79
cherrypy/_cpcgifs.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import cgi
|
||||
import cherrypy
|
||||
|
||||
|
||||
class FieldStorage(cgi.FieldStorage):
|
||||
def __init__(self, *args, **kwds):
|
||||
try:
|
||||
cgi.FieldStorage.__init__(self, *args, **kwds)
|
||||
except ValueError, ex:
|
||||
if str(ex) == 'Maximum content length exceeded':
|
||||
raise cherrypy.HTTPError(status=413)
|
||||
else:
|
||||
raise ex
|
||||
|
||||
def read_lines_to_eof(self):
|
||||
"""Internal: read lines until EOF."""
|
||||
while 1:
|
||||
line = self.fp.readline(1<<16)
|
||||
if not line:
|
||||
self.done = -1
|
||||
break
|
||||
self.__write(line)
|
||||
|
||||
def read_lines_to_outerboundary(self):
|
||||
"""Internal: read lines until outerboundary."""
|
||||
next = "--" + self.outerboundary
|
||||
last = next + "--"
|
||||
delim = ""
|
||||
last_line_lfend = True
|
||||
while 1:
|
||||
line = self.fp.readline(1<<16)
|
||||
if not line:
|
||||
self.done = -1
|
||||
break
|
||||
if line[:2] == "--" and last_line_lfend:
|
||||
strippedline = line.strip()
|
||||
if strippedline == next:
|
||||
break
|
||||
if strippedline == last:
|
||||
self.done = 1
|
||||
break
|
||||
odelim = delim
|
||||
if line[-2:] == "\r\n":
|
||||
delim = "\r\n"
|
||||
line = line[:-2]
|
||||
last_line_lfend = True
|
||||
elif line[-1] == "\n":
|
||||
delim = "\n"
|
||||
line = line[:-1]
|
||||
last_line_lfend = True
|
||||
else:
|
||||
delim = ""
|
||||
last_line_lfend = False
|
||||
self.__write(odelim + line)
|
||||
|
||||
def skip_lines(self):
|
||||
"""Internal: skip lines until outer boundary if defined."""
|
||||
if not self.outerboundary or self.done:
|
||||
return
|
||||
next = "--" + self.outerboundary
|
||||
last = next + "--"
|
||||
last_line_lfend = True
|
||||
while 1:
|
||||
line = self.fp.readline(1<<16)
|
||||
if not line:
|
||||
self.done = -1
|
||||
break
|
||||
if line[:2] == "--" and last_line_lfend:
|
||||
strippedline = line.strip()
|
||||
if strippedline == next:
|
||||
break
|
||||
if strippedline == last:
|
||||
self.done = 1
|
||||
break
|
||||
if line.endswith('\n'):
|
||||
last_line_lfend = True
|
||||
else:
|
||||
last_line_lfend = False
|
||||
|
||||
285
cherrypy/_cpchecker.py
Normal file
@@ -0,0 +1,285 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
class Checker(object):
|
||||
"""A checker for CherryPy sites and their mounted applications.
|
||||
|
||||
on: set this to False to turn off the checker completely.
|
||||
|
||||
When this object is called at engine startup, it executes each
|
||||
of its own methods whose names start with "check_". If you wish
|
||||
to disable selected checks, simply add a line in your global
|
||||
config which sets the appropriate method to False:
|
||||
|
||||
[global]
|
||||
checker.check_skipped_app_config = False
|
||||
|
||||
You may also dynamically add or replace check_* methods in this way.
|
||||
"""
|
||||
|
||||
on = True
|
||||
|
||||
def __init__(self):
|
||||
self._populate_known_types()
|
||||
|
||||
def __call__(self):
|
||||
"""Run all check_* methods."""
|
||||
if self.on:
|
||||
oldformatwarning = warnings.formatwarning
|
||||
warnings.formatwarning = self.formatwarning
|
||||
try:
|
||||
for name in dir(self):
|
||||
if name.startswith("check_"):
|
||||
method = getattr(self, name)
|
||||
if method and callable(method):
|
||||
method()
|
||||
finally:
|
||||
warnings.formatwarning = oldformatwarning
|
||||
|
||||
def formatwarning(self, message, category, filename, lineno, line=None):
|
||||
"""Function to format a warning."""
|
||||
return "CherryPy Checker:\n%s\n\n" % message
|
||||
|
||||
# This value should be set inside _cpconfig.
|
||||
global_config_contained_paths = False
|
||||
|
||||
def check_app_config_entries_dont_start_with_script_name(self):
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
if not app.config:
|
||||
continue
|
||||
if sn == '':
|
||||
continue
|
||||
sn_atoms = sn.strip("/").split("/")
|
||||
for key in app.config.keys():
|
||||
key_atoms = key.strip("/").split("/")
|
||||
if key_atoms[:len(sn_atoms)] == sn_atoms:
|
||||
warnings.warn(
|
||||
"The application mounted at %r has config " \
|
||||
"entries that start with its script name: %r" % (sn, key))
|
||||
|
||||
def check_skipped_app_config(self):
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
if not app.config:
|
||||
msg = "The Application mounted at %r has an empty config." % sn
|
||||
if self.global_config_contained_paths:
|
||||
msg += (" It looks like the config you passed to "
|
||||
"cherrypy.config.update() contains application-"
|
||||
"specific sections. You must explicitly pass "
|
||||
"application config via "
|
||||
"cherrypy.tree.mount(..., config=app_config)")
|
||||
warnings.warn(msg)
|
||||
return
|
||||
|
||||
def check_static_paths(self):
|
||||
# Use the dummy Request object in the main thread.
|
||||
request = cherrypy.request
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
request.app = app
|
||||
for section in app.config:
|
||||
# get_resource will populate request.config
|
||||
request.get_resource(section + "/dummy.html")
|
||||
conf = request.config.get
|
||||
|
||||
if conf("tools.staticdir.on", False):
|
||||
msg = ""
|
||||
root = conf("tools.staticdir.root")
|
||||
dir = conf("tools.staticdir.dir")
|
||||
if dir is None:
|
||||
msg = "tools.staticdir.dir is not set."
|
||||
else:
|
||||
fulldir = ""
|
||||
if os.path.isabs(dir):
|
||||
fulldir = dir
|
||||
if root:
|
||||
msg = ("dir is an absolute path, even "
|
||||
"though a root is provided.")
|
||||
testdir = os.path.join(root, dir[1:])
|
||||
if os.path.exists(testdir):
|
||||
msg += ("\nIf you meant to serve the "
|
||||
"filesystem folder at %r, remove "
|
||||
"the leading slash from dir." % testdir)
|
||||
else:
|
||||
if not root:
|
||||
msg = "dir is a relative path and no root provided."
|
||||
else:
|
||||
fulldir = os.path.join(root, dir)
|
||||
if not os.path.isabs(fulldir):
|
||||
msg = "%r is not an absolute path." % fulldir
|
||||
|
||||
if fulldir and not os.path.exists(fulldir):
|
||||
if msg:
|
||||
msg += "\n"
|
||||
msg += ("%r (root + dir) is not an existing "
|
||||
"filesystem path." % fulldir)
|
||||
|
||||
if msg:
|
||||
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
|
||||
% (msg, section, root, dir))
|
||||
|
||||
|
||||
# -------------------------- Compatibility -------------------------- #
|
||||
|
||||
obsolete = {
|
||||
'server.default_content_type': 'tools.response_headers.headers',
|
||||
'log_access_file': 'log.access_file',
|
||||
'log_config_options': None,
|
||||
'log_file': 'log.error_file',
|
||||
'log_file_not_found': None,
|
||||
'log_request_headers': 'tools.log_headers.on',
|
||||
'log_to_screen': 'log.screen',
|
||||
'show_tracebacks': 'request.show_tracebacks',
|
||||
'throw_errors': 'request.throw_errors',
|
||||
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
|
||||
'cherrypy.Application(Root())))'),
|
||||
}
|
||||
|
||||
deprecated = {}
|
||||
|
||||
def _compat(self, config):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
for section, conf in config.iteritems():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.iteritems():
|
||||
if k in self.obsolete:
|
||||
warnings.warn("%r is obsolete. Use %r instead.\n"
|
||||
"section: [%s]" %
|
||||
(k, self.obsolete[k], section))
|
||||
elif k in self.deprecated:
|
||||
warnings.warn("%r is deprecated. Use %r instead.\n"
|
||||
"section: [%s]" %
|
||||
(k, self.deprecated[k], section))
|
||||
else:
|
||||
if section in self.obsolete:
|
||||
warnings.warn("%r is obsolete. Use %r instead."
|
||||
% (section, self.obsolete[section]))
|
||||
elif section in self.deprecated:
|
||||
warnings.warn("%r is deprecated. Use %r instead."
|
||||
% (section, self.deprecated[section]))
|
||||
|
||||
def check_compatibility(self):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
self._compat(cherrypy.config)
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._compat(app.config)
|
||||
|
||||
|
||||
# ------------------------ Known Namespaces ------------------------ #
|
||||
|
||||
extra_config_namespaces = []
|
||||
|
||||
def _known_ns(self, app):
|
||||
ns = ["wsgi"]
|
||||
ns.extend(app.toolboxes.keys())
|
||||
ns.extend(app.namespaces.keys())
|
||||
ns.extend(app.request_class.namespaces.keys())
|
||||
ns.extend(cherrypy.config.namespaces.keys())
|
||||
ns += self.extra_config_namespaces
|
||||
|
||||
for section, conf in app.config.iteritems():
|
||||
is_path_section = section.startswith("/")
|
||||
if is_path_section and isinstance(conf, dict):
|
||||
for k, v in conf.iteritems():
|
||||
atoms = k.split(".")
|
||||
if len(atoms) > 1:
|
||||
if atoms[0] not in ns:
|
||||
# Spit out a special warning if a known
|
||||
# namespace is preceded by "cherrypy."
|
||||
if (atoms[0] == "cherrypy" and atoms[1] in ns):
|
||||
msg = ("The config entry %r is invalid; "
|
||||
"try %r instead.\nsection: [%s]"
|
||||
% (k, ".".join(atoms[1:]), section))
|
||||
else:
|
||||
msg = ("The config entry %r is invalid, because "
|
||||
"the %r config namespace is unknown.\n"
|
||||
"section: [%s]" % (k, atoms[0], section))
|
||||
warnings.warn(msg)
|
||||
elif atoms[0] == "tools":
|
||||
if atoms[1] not in dir(cherrypy.tools):
|
||||
msg = ("The config entry %r may be invalid, "
|
||||
"because the %r tool was not found.\n"
|
||||
"section: [%s]" % (k, atoms[1], section))
|
||||
warnings.warn(msg)
|
||||
|
||||
def check_config_namespaces(self):
|
||||
"""Process config and warn on each unknown config namespace."""
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_ns(app)
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------------- Config Types -------------------------- #
|
||||
|
||||
known_config_types = {}
|
||||
|
||||
def _populate_known_types(self):
|
||||
import __builtin__
|
||||
builtins = [x for x in vars(__builtin__).values()
|
||||
if type(x) is type(str)]
|
||||
|
||||
def traverse(obj, namespace):
|
||||
for name in dir(obj):
|
||||
vtype = type(getattr(obj, name, None))
|
||||
if vtype in builtins:
|
||||
self.known_config_types[namespace + "." + name] = vtype
|
||||
|
||||
traverse(cherrypy.request, "request")
|
||||
traverse(cherrypy.response, "response")
|
||||
traverse(cherrypy.server, "server")
|
||||
traverse(cherrypy.engine, "engine")
|
||||
traverse(cherrypy.log, "log")
|
||||
|
||||
def _known_types(self, config):
|
||||
msg = ("The config entry %r in section %r is of type %r, "
|
||||
"which does not match the expected type %r.")
|
||||
|
||||
for section, conf in config.iteritems():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.iteritems():
|
||||
if v is not None:
|
||||
expected_type = self.known_config_types.get(k, None)
|
||||
vtype = type(v)
|
||||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
else:
|
||||
k, v = section, conf
|
||||
if v is not None:
|
||||
expected_type = self.known_config_types.get(k, None)
|
||||
vtype = type(v)
|
||||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
|
||||
def check_config_types(self):
|
||||
"""Assert that config values are of the same type as default values."""
|
||||
self._known_types(cherrypy.config)
|
||||
for sn, app in cherrypy.tree.apps.iteritems():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_types(app.config)
|
||||
|
||||
|
||||
# -------------------- Specific config warnings -------------------- #
|
||||
|
||||
def check_localhost(self):
|
||||
"""Warn if any socket_host is 'localhost'. See #711."""
|
||||
for k, v in cherrypy.config.iteritems():
|
||||
if k == 'server.socket_host' and v == 'localhost':
|
||||
warnings.warn("The use of 'localhost' as a socket host can "
|
||||
"cause problems on newer systems, since 'localhost' can "
|
||||
"map to either an IPv4 or an IPv6 address. You should "
|
||||
"use '127.0.0.1' or '[::1]' instead.")
|
||||
415
cherrypy/_cpconfig.py
Normal file
@@ -0,0 +1,415 @@
|
||||
"""Configuration system for CherryPy.
|
||||
|
||||
Configuration in CherryPy is implemented via dictionaries. Keys are strings
|
||||
which name the mapped value, which may be of any type.
|
||||
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
CherryPy Requests are part of an Application, which runs in a global context,
|
||||
and configuration data may apply to any of those three scopes:
|
||||
|
||||
Global: configuration entries which apply everywhere are stored in
|
||||
cherrypy.config.
|
||||
|
||||
Application: entries which apply to each mounted application are stored
|
||||
on the Application object itself, as 'app.config'. This is a two-level
|
||||
dict where each key is a path, or "relative URL" (for example, "/" or
|
||||
"/path/to/my/page"), and each value is a config dict. Usually, this
|
||||
data is provided in the call to tree.mount(root(), config=conf),
|
||||
although you may also use app.merge(conf).
|
||||
|
||||
Request: each Request object possesses a single 'Request.config' dict.
|
||||
Early in the request process, this dict is populated by merging global
|
||||
config entries, Application entries (whose path equals or is a parent
|
||||
of Request.path_info), and any config acquired while looking up the
|
||||
page handler (see next).
|
||||
|
||||
|
||||
Declaration
|
||||
-----------
|
||||
|
||||
Configuration data may be supplied as a Python dictionary, as a filename,
|
||||
or as an open file object. When you supply a filename or file, CherryPy
|
||||
uses Python's builtin ConfigParser; you declare Application config by
|
||||
writing each path as a section header:
|
||||
|
||||
[/path/to/my/page]
|
||||
request.stream = True
|
||||
|
||||
To declare global configuration entries, place them in a [global] section.
|
||||
|
||||
You may also declare config entries directly on the classes and methods
|
||||
(page handlers) that make up your CherryPy application via the '_cp_config'
|
||||
attribute. For example:
|
||||
|
||||
class Demo:
|
||||
_cp_config = {'tools.gzip.on': True}
|
||||
|
||||
def index(self):
|
||||
return "Hello world"
|
||||
index.exposed = True
|
||||
index._cp_config = {'request.show_tracebacks': False}
|
||||
|
||||
Note, however, that this behavior is only guaranteed for the default
|
||||
dispatcher. Other dispatchers may have different restrictions on where
|
||||
you can attach _cp_config attributes.
|
||||
|
||||
|
||||
Namespaces
|
||||
----------
|
||||
|
||||
Configuration keys are separated into namespaces by the first "." in the key.
|
||||
Current namespaces:
|
||||
|
||||
engine: Controls the 'application engine', including autoreload.
|
||||
These can only be declared in the global config.
|
||||
tree: Grafts cherrypy.Application objects onto cherrypy.tree.
|
||||
These can only be declared in the global config.
|
||||
hooks: Declares additional request-processing functions.
|
||||
log: Configures the logging for each application.
|
||||
These can only be declared in the global or / config.
|
||||
request: Adds attributes to each Request.
|
||||
response: Adds attributes to each Response.
|
||||
server: Controls the default HTTP server via cherrypy.server.
|
||||
These can only be declared in the global config.
|
||||
tools: Runs and configures additional request-processing packages.
|
||||
wsgi: Adds WSGI middleware to an Application's "pipeline".
|
||||
These can only be declared in the app's root config ("/").
|
||||
checker: Controls the 'checker', which looks for common errors in
|
||||
app state (including config) when the engine starts.
|
||||
Global config only.
|
||||
|
||||
The only key that does not exist in a namespace is the "environment" entry.
|
||||
This special entry 'imports' other config entries from a template stored in
|
||||
cherrypy._cpconfig.environments[environment]. It only applies to the global
|
||||
config, and only when you use cherrypy.config.update.
|
||||
|
||||
You can define your own namespaces to be called at the Global, Application,
|
||||
or Request level, by adding a named handler to cherrypy.config.namespaces,
|
||||
app.namespaces, or app.request_class.namespaces. The name can
|
||||
be any string, and the handler must be either a callable or a (Python 2.5
|
||||
style) context manager.
|
||||
"""
|
||||
|
||||
import ConfigParser
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
environments = {
|
||||
"staging": {
|
||||
'engine.autoreload_on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
},
|
||||
"production": {
|
||||
'engine.autoreload_on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
'log.screen': False,
|
||||
},
|
||||
"embedded": {
|
||||
# For use with CherryPy embedded in another deployment stack.
|
||||
'engine.autoreload_on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
'log.screen': False,
|
||||
'engine.SIGHUP': None,
|
||||
'engine.SIGTERM': None,
|
||||
},
|
||||
"test_suite": {
|
||||
'engine.autoreload_on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': True,
|
||||
'request.show_mismatched_params': True,
|
||||
'log.screen': False,
|
||||
},
|
||||
}
|
||||
|
||||
def as_dict(config):
|
||||
"""Return a dict from 'config' whether it is a dict, file, or filename."""
|
||||
if isinstance(config, basestring):
|
||||
config = _Parser().dict_from_file(config)
|
||||
elif hasattr(config, 'read'):
|
||||
config = _Parser().dict_from_file(config)
|
||||
return config
|
||||
|
||||
def merge(base, other):
|
||||
"""Merge one app config (from a dict, file, or filename) into another.
|
||||
|
||||
If the given config is a filename, it will be appended to
|
||||
the list of files to monitor for "autoreload" changes.
|
||||
"""
|
||||
if isinstance(other, basestring):
|
||||
cherrypy.engine.autoreload.files.add(other)
|
||||
|
||||
# Load other into base
|
||||
for section, value_map in as_dict(other).iteritems():
|
||||
base.setdefault(section, {}).update(value_map)
|
||||
|
||||
|
||||
class NamespaceSet(dict):
|
||||
"""A dict of config namespace names and handlers.
|
||||
|
||||
Each config entry should begin with a namespace name; the corresponding
|
||||
namespace handler will be called once for each config entry in that
|
||||
namespace, and will be passed two arguments: the config key (with the
|
||||
namespace removed) and the config value.
|
||||
|
||||
Namespace handlers may be any Python callable; they may also be
|
||||
Python 2.5-style 'context managers', in which case their __enter__
|
||||
method should return a callable to be used as the handler.
|
||||
See cherrypy.tools (the Toolbox class) for an example.
|
||||
"""
|
||||
|
||||
def __call__(self, config):
|
||||
"""Iterate through config and pass it to each namespace handler.
|
||||
|
||||
'config' should be a flat dict, where keys use dots to separate
|
||||
namespaces, and values are arbitrary.
|
||||
|
||||
The first name in each config key is used to look up the corresponding
|
||||
namespace handler. For example, a config entry of {'tools.gzip.on': v}
|
||||
will call the 'tools' namespace handler with the args: ('gzip.on', v)
|
||||
"""
|
||||
# Separate the given config into namespaces
|
||||
ns_confs = {}
|
||||
for k in config:
|
||||
if "." in k:
|
||||
ns, name = k.split(".", 1)
|
||||
bucket = ns_confs.setdefault(ns, {})
|
||||
bucket[name] = config[k]
|
||||
|
||||
# I chose __enter__ and __exit__ so someday this could be
|
||||
# rewritten using Python 2.5's 'with' statement:
|
||||
# for ns, handler in self.iteritems():
|
||||
# with handler as callable:
|
||||
# for k, v in ns_confs.get(ns, {}).iteritems():
|
||||
# callable(k, v)
|
||||
for ns, handler in self.iteritems():
|
||||
exit = getattr(handler, "__exit__", None)
|
||||
if exit:
|
||||
callable = handler.__enter__()
|
||||
no_exc = True
|
||||
try:
|
||||
try:
|
||||
for k, v in ns_confs.get(ns, {}).iteritems():
|
||||
callable(k, v)
|
||||
except:
|
||||
# The exceptional case is handled here
|
||||
no_exc = False
|
||||
if exit is None:
|
||||
raise
|
||||
if not exit(*sys.exc_info()):
|
||||
raise
|
||||
# The exception is swallowed if exit() returns true
|
||||
finally:
|
||||
# The normal and non-local-goto cases are handled here
|
||||
if no_exc and exit:
|
||||
exit(None, None, None)
|
||||
else:
|
||||
for k, v in ns_confs.get(ns, {}).iteritems():
|
||||
handler(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
|
||||
dict.__repr__(self))
|
||||
|
||||
def __copy__(self):
|
||||
newobj = self.__class__()
|
||||
newobj.update(self)
|
||||
return newobj
|
||||
copy = __copy__
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""The 'global' configuration data for the entire CherryPy process."""
|
||||
|
||||
defaults = {
|
||||
'tools.log_tracebacks.on': True,
|
||||
'tools.log_headers.on': True,
|
||||
'tools.trailing_slash.on': True,
|
||||
}
|
||||
|
||||
namespaces = NamespaceSet(
|
||||
**{"log": lambda k, v: setattr(cherrypy.log, k, v),
|
||||
"checker": lambda k, v: setattr(cherrypy.checker, k, v),
|
||||
})
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""Reset self to default values."""
|
||||
self.clear()
|
||||
dict.update(self, self.defaults)
|
||||
|
||||
def update(self, config):
|
||||
"""Update self from a dict, file or filename."""
|
||||
if isinstance(config, basestring):
|
||||
# Filename
|
||||
cherrypy.engine.autoreload.files.add(config)
|
||||
config = _Parser().dict_from_file(config)
|
||||
elif hasattr(config, 'read'):
|
||||
# Open file object
|
||||
config = _Parser().dict_from_file(config)
|
||||
else:
|
||||
config = config.copy()
|
||||
|
||||
if isinstance(config.get("global", None), dict):
|
||||
if len(config) > 1:
|
||||
cherrypy.checker.global_config_contained_paths = True
|
||||
config = config["global"]
|
||||
|
||||
which_env = config.get('environment')
|
||||
if which_env:
|
||||
env = environments[which_env]
|
||||
for k in env:
|
||||
if k not in config:
|
||||
config[k] = env[k]
|
||||
|
||||
if 'tools.staticdir.dir' in config:
|
||||
config['tools.staticdir.section'] = "global"
|
||||
|
||||
dict.update(self, config)
|
||||
self.namespaces(config)
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
dict.__setitem__(self, k, v)
|
||||
self.namespaces({k: v})
|
||||
|
||||
|
||||
def _server_namespace_handler(k, v):
|
||||
"""Config handler for the "server" namespace."""
|
||||
atoms = k.split(".", 1)
|
||||
if len(atoms) > 1:
|
||||
# Special-case config keys of the form 'server.servername.socket_port'
|
||||
# to configure additional HTTP servers.
|
||||
if not hasattr(cherrypy, "servers"):
|
||||
cherrypy.servers = {}
|
||||
|
||||
servername, k = atoms
|
||||
if servername not in cherrypy.servers:
|
||||
from cherrypy import _cpserver
|
||||
cherrypy.servers[servername] = _cpserver.Server()
|
||||
# On by default, but 'on = False' can unsubscribe it (see below).
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
||||
if k == 'on':
|
||||
if v:
|
||||
cherrypy.servers[servername].subscribe()
|
||||
else:
|
||||
cherrypy.servers[servername].unsubscribe()
|
||||
else:
|
||||
setattr(cherrypy.servers[servername], k, v)
|
||||
else:
|
||||
setattr(cherrypy.server, k, v)
|
||||
Config.namespaces["server"] = _server_namespace_handler
|
||||
|
||||
def _engine_namespace_handler(k, v):
|
||||
"""Backward compatibility handler for the "engine" namespace."""
|
||||
engine = cherrypy.engine
|
||||
if k == 'autoreload_on':
|
||||
if v:
|
||||
engine.autoreload.subscribe()
|
||||
else:
|
||||
engine.autoreload.unsubscribe()
|
||||
elif k == 'autoreload_frequency':
|
||||
engine.autoreload.frequency = v
|
||||
elif k == 'autoreload_match':
|
||||
engine.autoreload.match = v
|
||||
elif k == 'reload_files':
|
||||
engine.autoreload.files = set(v)
|
||||
elif k == 'deadlock_poll_freq':
|
||||
engine.timeout_monitor.frequency = v
|
||||
elif k == 'SIGHUP':
|
||||
engine.listeners['SIGHUP'] = set([v])
|
||||
elif k == 'SIGTERM':
|
||||
engine.listeners['SIGTERM'] = set([v])
|
||||
elif "." in k:
|
||||
plugin, attrname = k.split(".", 1)
|
||||
plugin = getattr(engine, plugin)
|
||||
if attrname == 'on':
|
||||
if v and callable(getattr(plugin, 'subscribe', None)):
|
||||
plugin.subscribe()
|
||||
return
|
||||
elif (not v) and callable(getattr(plugin, 'unsubscribe', None)):
|
||||
plugin.unsubscribe()
|
||||
return
|
||||
setattr(plugin, attrname, v)
|
||||
else:
|
||||
setattr(engine, k, v)
|
||||
Config.namespaces["engine"] = _engine_namespace_handler
|
||||
|
||||
|
||||
def _tree_namespace_handler(k, v):
|
||||
"""Namespace handler for the 'tree' config namespace."""
|
||||
cherrypy.tree.graft(v, v.script_name)
|
||||
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
|
||||
Config.namespaces["tree"] = _tree_namespace_handler
|
||||
|
||||
|
||||
class _Parser(ConfigParser.ConfigParser):
|
||||
"""Sub-class of ConfigParser that keeps the case of options and that raises
|
||||
an exception if the file cannot be read.
|
||||
"""
|
||||
|
||||
def optionxform(self, optionstr):
|
||||
return optionstr
|
||||
|
||||
def read(self, filenames):
|
||||
if isinstance(filenames, basestring):
|
||||
filenames = [filenames]
|
||||
for filename in filenames:
|
||||
# try:
|
||||
# fp = open(filename)
|
||||
# except IOError:
|
||||
# continue
|
||||
fp = open(filename)
|
||||
try:
|
||||
self._read(fp, filename)
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
def as_dict(self, raw=False, vars=None):
|
||||
"""Convert an INI file to a dictionary"""
|
||||
# Load INI file into a dict
|
||||
from cherrypy.lib import unrepr
|
||||
result = {}
|
||||
for section in self.sections():
|
||||
if section not in result:
|
||||
result[section] = {}
|
||||
for option in self.options(section):
|
||||
value = self.get(section, option, raw, vars)
|
||||
try:
|
||||
value = unrepr(value)
|
||||
except Exception, x:
|
||||
msg = ("Config error in section: %r, option: %r, "
|
||||
"value: %r. Config values must be valid Python." %
|
||||
(section, option, value))
|
||||
raise ValueError(msg, x.__class__.__name__, x.args)
|
||||
result[section][option] = value
|
||||
return result
|
||||
|
||||
def dict_from_file(self, file):
|
||||
if hasattr(file, 'read'):
|
||||
self.readfp(file)
|
||||
else:
|
||||
self.read(file)
|
||||
return self.as_dict()
|
||||
|
||||
del ConfigParser
|
||||
561
cherrypy/_cpdispatch.py
Normal file
@@ -0,0 +1,561 @@
|
||||
"""CherryPy dispatchers.
|
||||
|
||||
A 'dispatcher' is the object which looks up the 'page handler' callable
|
||||
and collects config for the current request based on the path_info, other
|
||||
request attributes, and the application architecture. The core calls the
|
||||
dispatcher as early as possible, passing it a 'path_info' argument.
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
"""
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
class PageHandler(object):
|
||||
"""Callable which sets response.body."""
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
self.callable = callable
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
return self.callable(*self.args, **self.kwargs)
|
||||
except TypeError, x:
|
||||
try:
|
||||
test_callable_spec(self.callable, self.args, self.kwargs)
|
||||
except cherrypy.HTTPError, error:
|
||||
raise error
|
||||
except:
|
||||
raise x
|
||||
raise
|
||||
|
||||
|
||||
def test_callable_spec(callable, callable_args, callable_kwargs):
|
||||
"""
|
||||
Inspect callable and test to see if the given args are suitable for it.
|
||||
|
||||
When an error occurs during the handler's invoking stage there are 2
|
||||
erroneous cases:
|
||||
1. Too many parameters passed to a function which doesn't define
|
||||
one of *args or **kwargs.
|
||||
2. Too little parameters are passed to the function.
|
||||
|
||||
There are 3 sources of parameters to a cherrypy handler.
|
||||
1. query string parameters are passed as keyword parameters to the handler.
|
||||
2. body parameters are also passed as keyword parameters.
|
||||
3. when partial matching occurs, the final path atoms are passed as
|
||||
positional args.
|
||||
Both the query string and path atoms are part of the URI. If they are
|
||||
incorrect, then a 404 Not Found should be raised. Conversely the body
|
||||
parameters are part of the request; if they are invalid a 400 Bad Request.
|
||||
"""
|
||||
show_mismatched_params = getattr(cherrypy.request, 'show_mismatched_params', False)
|
||||
try:
|
||||
(args, varargs, varkw, defaults) = inspect.getargspec(callable)
|
||||
except TypeError:
|
||||
if isinstance(callable, object) and hasattr(callable, '__call__'):
|
||||
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
|
||||
else:
|
||||
# If it wasn't one of our own types, re-raise
|
||||
# the original error
|
||||
raise
|
||||
|
||||
if args and args[0] == 'self':
|
||||
args = args[1:]
|
||||
|
||||
arg_usage = dict([(arg, 0,) for arg in args])
|
||||
vararg_usage = 0
|
||||
varkw_usage = 0
|
||||
extra_kwargs = set()
|
||||
|
||||
for i, value in enumerate(callable_args):
|
||||
try:
|
||||
arg_usage[args[i]] += 1
|
||||
except IndexError:
|
||||
vararg_usage += 1
|
||||
|
||||
for key in callable_kwargs.keys():
|
||||
try:
|
||||
arg_usage[key] += 1
|
||||
except KeyError:
|
||||
varkw_usage += 1
|
||||
extra_kwargs.add(key)
|
||||
|
||||
for i, val in enumerate(defaults or []):
|
||||
# Defaults take effect only when the arg hasn't been used yet.
|
||||
if arg_usage[args[i]] == 0:
|
||||
arg_usage[args[i]] += 1
|
||||
|
||||
missing_args = []
|
||||
multiple_args = []
|
||||
for key, usage in arg_usage.iteritems():
|
||||
if usage == 0:
|
||||
missing_args.append(key)
|
||||
elif usage > 1:
|
||||
multiple_args.append(key)
|
||||
|
||||
if missing_args:
|
||||
# In the case where the method allows body arguments
|
||||
# there are 3 potential errors:
|
||||
# 1. not enough query string parameters -> 404
|
||||
# 2. not enough body parameters -> 400
|
||||
# 3. not enough path parts (partial matches) -> 404
|
||||
#
|
||||
# We can't actually tell which case it is,
|
||||
# so I'm raising a 404 because that covers 2/3 of the
|
||||
# possibilities
|
||||
#
|
||||
# In the case where the method does not allow body
|
||||
# arguments it's definitely a 404.
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Missing parameters: %s" % ",".join(missing_args)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# the extra positional arguments come from the path - 404 Not Found
|
||||
if not varargs and vararg_usage > 0:
|
||||
raise cherrypy.HTTPError(404)
|
||||
|
||||
body_params = cherrypy.request.body_params or {}
|
||||
body_params = set(body_params.keys())
|
||||
qs_params = set(callable_kwargs.keys()) - body_params
|
||||
|
||||
if multiple_args:
|
||||
|
||||
if qs_params.intersection(set(multiple_args)):
|
||||
# If any of the multiple parameters came from the query string then
|
||||
# it's a 404 Not Found
|
||||
error = 404
|
||||
else:
|
||||
# Otherwise it's a 400 Bad Request
|
||||
error = 400
|
||||
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Multiple values for parameters: "\
|
||||
"%s" % ",".join(multiple_args)
|
||||
raise cherrypy.HTTPError(error, message=message)
|
||||
|
||||
if not varkw and varkw_usage > 0:
|
||||
|
||||
# If there were extra query string parameters, it's a 404 Not Found
|
||||
extra_qs_params = set(qs_params).intersection(extra_kwargs)
|
||||
if extra_qs_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Unexpected query string "\
|
||||
"parameters: %s" % ", ".join(extra_qs_params)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# If there were any extra body parameters, it's a 400 Not Found
|
||||
extra_body_params = set(body_params).intersection(extra_kwargs)
|
||||
if extra_body_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Unexpected body parameters: "\
|
||||
"%s" % ", ".join(extra_body_params)
|
||||
raise cherrypy.HTTPError(400, message=message)
|
||||
|
||||
|
||||
try:
|
||||
import inspect
|
||||
except ImportError:
|
||||
test_callable_spec = lambda callable, args, kwargs: None
|
||||
|
||||
|
||||
|
||||
class LateParamPageHandler(PageHandler):
|
||||
"""When passing cherrypy.request.params to the page handler, we do not
|
||||
want to capture that dict too early; we want to give tools like the
|
||||
decoding tool a chance to modify the params dict in-between the lookup
|
||||
of the handler and the actual calling of the handler. This subclass
|
||||
takes that into account, and allows request.params to be 'bound late'
|
||||
(it's more complicated than that, but that's the effect).
|
||||
"""
|
||||
|
||||
def _get_kwargs(self):
|
||||
kwargs = cherrypy.request.params.copy()
|
||||
if self._kwargs:
|
||||
kwargs.update(self._kwargs)
|
||||
return kwargs
|
||||
|
||||
def _set_kwargs(self, kwargs):
|
||||
self._kwargs = kwargs
|
||||
|
||||
kwargs = property(_get_kwargs, _set_kwargs,
|
||||
doc='page handler kwargs (with '
|
||||
'cherrypy.request.params copied in)')
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
|
||||
|
||||
The tree is rooted at cherrypy.request.app.root, and each hierarchical
|
||||
component in the path_info argument is matched to a corresponding nested
|
||||
attribute of the root object. Matching handlers must have an 'exposed'
|
||||
attribute which evaluates to True. The special method name "index"
|
||||
matches a URI which ends in a slash ("/"). The special method name
|
||||
"default" may match a portion of the path_info (but only when no longer
|
||||
substring of the path_info matches some other object).
|
||||
|
||||
This is the default, built-in dispatcher for CherryPy.
|
||||
"""
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
dispatch_method_name = '_cp_dispatch'
|
||||
dispatch_method_name__doc = """
|
||||
The name of the dispatch method that nodes may optionally implement
|
||||
to provide their own dynamic dispatch algorithm.
|
||||
"""
|
||||
|
||||
def __init__(self, dispatch_method_name = None):
|
||||
if dispatch_method_name:
|
||||
self.dispatch_method_name = dispatch_method_name
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.request
|
||||
func, vpath = self.find_handler(path_info)
|
||||
|
||||
if func:
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
def find_handler(self, path):
|
||||
"""Return the appropriate page handler, plus any virtual path.
|
||||
|
||||
This will return two objects. The first will be a callable,
|
||||
which can be used to generate page output. Any parameters from
|
||||
the query string or request body will be sent to that callable
|
||||
as keyword arguments.
|
||||
|
||||
The callable is found by traversing the application's tree,
|
||||
starting from cherrypy.request.app.root, and matching path
|
||||
components to successive objects in the tree. For example, the
|
||||
URL "/path/to/handler" might return root.path.to.handler.
|
||||
|
||||
The second object returned will be a list of names which are
|
||||
'virtual path' components: parts of the URL which are dynamic,
|
||||
and were not used when looking up the handler.
|
||||
These virtual path components are passed to the handler as
|
||||
positional arguments.
|
||||
"""
|
||||
request = cherrypy.request
|
||||
app = request.app
|
||||
root = app.root
|
||||
dispatch_name = self.dispatch_method_name
|
||||
|
||||
# Get config for the root object/path.
|
||||
curpath = ""
|
||||
nodeconf = {}
|
||||
if hasattr(root, "_cp_config"):
|
||||
nodeconf.update(root._cp_config)
|
||||
if "/" in app.config:
|
||||
nodeconf.update(app.config["/"])
|
||||
object_trail = [['root', root, nodeconf, curpath]]
|
||||
|
||||
node = root
|
||||
names = [x for x in path.strip('/').split('/') if x] + ['index']
|
||||
iternames = names[:]
|
||||
while iternames:
|
||||
name = iternames[0]
|
||||
# map to legal Python identifiers (replace '.' with '_')
|
||||
objname = name.replace('.', '_')
|
||||
|
||||
nodeconf = {}
|
||||
subnode = getattr(node, objname, None)
|
||||
if subnode is None:
|
||||
dispatch = getattr(node, dispatch_name, None)
|
||||
if dispatch and callable(dispatch) and not \
|
||||
getattr(dispatch, 'exposed', False):
|
||||
subnode = dispatch(vpath=iternames)
|
||||
name = iternames.pop(0)
|
||||
node = subnode
|
||||
|
||||
if node is not None:
|
||||
# Get _cp_config attached to this node.
|
||||
if hasattr(node, "_cp_config"):
|
||||
nodeconf.update(node._cp_config)
|
||||
|
||||
# Mix in values from app.config for this path.
|
||||
curpath = "/".join((curpath, name))
|
||||
if curpath in app.config:
|
||||
nodeconf.update(app.config[curpath])
|
||||
|
||||
object_trail.append([name, node, nodeconf, curpath])
|
||||
|
||||
def set_conf():
|
||||
"""Collapse all object_trail config into cherrypy.request.config."""
|
||||
base = cherrypy.config.copy()
|
||||
# Note that we merge the config from each node
|
||||
# even if that node was None.
|
||||
for name, obj, conf, curpath in object_trail:
|
||||
base.update(conf)
|
||||
if 'tools.staticdir.dir' in conf:
|
||||
base['tools.staticdir.section'] = curpath
|
||||
return base
|
||||
|
||||
# Try successive objects (reverse order)
|
||||
num_candidates = len(object_trail) - 1
|
||||
for i in xrange(num_candidates, -1, -1):
|
||||
|
||||
name, candidate, nodeconf, curpath = object_trail[i]
|
||||
if candidate is None:
|
||||
continue
|
||||
|
||||
# Try a "default" method on the current leaf.
|
||||
if hasattr(candidate, "default"):
|
||||
defhandler = candidate.default
|
||||
if getattr(defhandler, 'exposed', False):
|
||||
# Insert any extra _cp_config from the default handler.
|
||||
conf = getattr(defhandler, "_cp_config", {})
|
||||
object_trail.insert(i+1, ["default", defhandler, conf, curpath])
|
||||
request.config = set_conf()
|
||||
# See http://www.cherrypy.org/ticket/613
|
||||
request.is_index = path.endswith("/")
|
||||
return defhandler, names[i:-1]
|
||||
|
||||
# Uncomment the next line to restrict positional params to "default".
|
||||
# if i < num_candidates - 2: continue
|
||||
|
||||
# Try the current leaf.
|
||||
if getattr(candidate, 'exposed', False):
|
||||
request.config = set_conf()
|
||||
if i == num_candidates:
|
||||
# We found the extra ".index". Mark request so tools
|
||||
# can redirect if path_info has no trailing slash.
|
||||
request.is_index = True
|
||||
else:
|
||||
# We're not at an 'index' handler. Mark request so tools
|
||||
# can redirect if path_info has NO trailing slash.
|
||||
# Note that this also includes handlers which take
|
||||
# positional parameters (virtual paths).
|
||||
request.is_index = False
|
||||
return candidate, names[i:-1]
|
||||
|
||||
# We didn't find anything
|
||||
request.config = set_conf()
|
||||
return None, []
|
||||
|
||||
|
||||
class MethodDispatcher(Dispatcher):
|
||||
"""Additional dispatch based on cherrypy.request.method.upper().
|
||||
|
||||
Methods named GET, POST, etc will be called on an exposed class.
|
||||
The method names must be all caps; the appropriate Allow header
|
||||
will be output showing all capitalized method names as allowable
|
||||
HTTP verbs.
|
||||
|
||||
Note that the containing class must be exposed, not the methods.
|
||||
"""
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.request
|
||||
resource, vpath = self.find_handler(path_info)
|
||||
|
||||
if resource:
|
||||
# Set Allow header
|
||||
avail = [m for m in dir(resource) if m.isupper()]
|
||||
if "GET" in avail and "HEAD" not in avail:
|
||||
avail.append("HEAD")
|
||||
avail.sort()
|
||||
cherrypy.response.headers['Allow'] = ", ".join(avail)
|
||||
|
||||
# Find the subhandler
|
||||
meth = request.method.upper()
|
||||
func = getattr(resource, meth, None)
|
||||
if func is None and meth == "HEAD":
|
||||
func = getattr(resource, "GET", None)
|
||||
if func:
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.HTTPError(405)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
class RoutesDispatcher(object):
|
||||
"""A Routes based dispatcher for CherryPy."""
|
||||
|
||||
def __init__(self, full_result=False):
|
||||
"""
|
||||
Routes dispatcher
|
||||
|
||||
Set full_result to True if you wish the controller
|
||||
and the action to be passed on to the page handler
|
||||
parameters. By default they won't be.
|
||||
"""
|
||||
import routes
|
||||
self.full_result = full_result
|
||||
self.controllers = {}
|
||||
self.mapper = routes.Mapper()
|
||||
self.mapper.controller_scan = self.controllers.keys
|
||||
|
||||
def connect(self, name, route, controller, **kwargs):
|
||||
self.controllers[name] = controller
|
||||
self.mapper.connect(name, route, controller=name, **kwargs)
|
||||
|
||||
def redirect(self, url):
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
func = self.find_handler(path_info)
|
||||
if func:
|
||||
cherrypy.request.handler = LateParamPageHandler(func)
|
||||
else:
|
||||
cherrypy.request.handler = cherrypy.NotFound()
|
||||
|
||||
def find_handler(self, path_info):
|
||||
"""Find the right page handler, and set request.config."""
|
||||
import routes
|
||||
|
||||
request = cherrypy.request
|
||||
|
||||
config = routes.request_config()
|
||||
config.mapper = self.mapper
|
||||
if hasattr(cherrypy.request, 'wsgi_environ'):
|
||||
config.environ = cherrypy.request.wsgi_environ
|
||||
config.host = request.headers.get('Host', None)
|
||||
config.protocol = request.scheme
|
||||
config.redirect = self.redirect
|
||||
|
||||
result = self.mapper.match(path_info)
|
||||
|
||||
config.mapper_dict = result
|
||||
params = {}
|
||||
if result:
|
||||
params = result.copy()
|
||||
if not self.full_result:
|
||||
params.pop('controller', None)
|
||||
params.pop('action', None)
|
||||
request.params.update(params)
|
||||
|
||||
# Get config for the root object/path.
|
||||
request.config = base = cherrypy.config.copy()
|
||||
curpath = ""
|
||||
|
||||
def merge(nodeconf):
|
||||
if 'tools.staticdir.dir' in nodeconf:
|
||||
nodeconf['tools.staticdir.section'] = curpath or "/"
|
||||
base.update(nodeconf)
|
||||
|
||||
app = request.app
|
||||
root = app.root
|
||||
if hasattr(root, "_cp_config"):
|
||||
merge(root._cp_config)
|
||||
if "/" in app.config:
|
||||
merge(app.config["/"])
|
||||
|
||||
# Mix in values from app.config.
|
||||
atoms = [x for x in path_info.split("/") if x]
|
||||
if atoms:
|
||||
last = atoms.pop()
|
||||
else:
|
||||
last = None
|
||||
for atom in atoms:
|
||||
curpath = "/".join((curpath, atom))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
handler = None
|
||||
if result:
|
||||
controller = result.get('controller', None)
|
||||
controller = self.controllers.get(controller)
|
||||
if controller:
|
||||
# Get config from the controller.
|
||||
if hasattr(controller, "_cp_config"):
|
||||
merge(controller._cp_config)
|
||||
|
||||
action = result.get('action', None)
|
||||
if action is not None:
|
||||
handler = getattr(controller, action, None)
|
||||
# Get config from the handler
|
||||
if hasattr(handler, "_cp_config"):
|
||||
merge(handler._cp_config)
|
||||
|
||||
# Do the last path atom here so it can
|
||||
# override the controller's _cp_config.
|
||||
if last:
|
||||
curpath = "/".join((curpath, last))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
||||
from cherrypy.lib import xmlrpc
|
||||
def xmlrpc_dispatch(path_info):
|
||||
path_info = xmlrpc.patched_path(path_info)
|
||||
return next_dispatcher(path_info)
|
||||
return xmlrpc_dispatch
|
||||
|
||||
|
||||
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
|
||||
"""Select a different handler based on the Host header.
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different parts of a single
|
||||
website structure. For example:
|
||||
|
||||
http://www.domain.example -> root
|
||||
http://www.domain2.example -> root/domain2/
|
||||
http://www.domain2.example:443 -> root/secure
|
||||
|
||||
can be accomplished via the following config:
|
||||
|
||||
[/]
|
||||
request.dispatch = cherrypy.dispatch.VirtualHost(
|
||||
**{'www.domain2.example': '/domain2',
|
||||
'www.domain2.example:443': '/secure',
|
||||
})
|
||||
|
||||
next_dispatcher: the next dispatcher object in the dispatch chain.
|
||||
The VirtualHost dispatcher adds a prefix to the URL and calls
|
||||
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
|
||||
|
||||
use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying.
|
||||
|
||||
**domains: a dict of {host header value: virtual prefix} pairs.
|
||||
The incoming "Host" request header is looked up in this dict,
|
||||
and, if a match is found, the corresponding "virtual prefix"
|
||||
value will be prepended to the URL path before calling the
|
||||
next dispatcher. Note that you often need separate entries
|
||||
for "example.com" and "www.example.com". In addition, "Host"
|
||||
headers may contain the port number.
|
||||
"""
|
||||
from cherrypy.lib import http
|
||||
def vhost_dispatch(path_info):
|
||||
header = cherrypy.request.headers.get
|
||||
|
||||
domain = header('Host', '')
|
||||
if use_x_forwarded_host:
|
||||
domain = header("X-Forwarded-Host", domain)
|
||||
|
||||
prefix = domains.get(domain, "")
|
||||
if prefix:
|
||||
path_info = http.urljoin(prefix, path_info)
|
||||
|
||||
result = next_dispatcher(path_info)
|
||||
|
||||
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
|
||||
section = cherrypy.request.config.get('tools.staticdir.section')
|
||||
if section:
|
||||
section = section[len(prefix):]
|
||||
cherrypy.request.config['tools.staticdir.section'] = section
|
||||
|
||||
return result
|
||||
return vhost_dispatch
|
||||
|
||||
392
cherrypy/_cperror.py
Normal file
@@ -0,0 +1,392 @@
|
||||
"""Error classes for CherryPy."""
|
||||
|
||||
from cgi import escape as _escape
|
||||
from sys import exc_info as _exc_info
|
||||
from traceback import format_exception as _format_exception
|
||||
from urlparse import urljoin as _urljoin
|
||||
from cherrypy.lib import http as _http
|
||||
|
||||
|
||||
class CherryPyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutError(CherryPyException):
|
||||
"""Exception raised when Response.timed_out is detected."""
|
||||
pass
|
||||
|
||||
|
||||
class InternalRedirect(CherryPyException):
|
||||
"""Exception raised to switch to the handler for a different URL.
|
||||
|
||||
Any request.params must be supplied in a query string.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
import cherrypy
|
||||
request = cherrypy.request
|
||||
|
||||
self.query_string = ""
|
||||
if "?" in path:
|
||||
# Separate any params included in the path
|
||||
path, self.query_string = path.split("?", 1)
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a URL relative to root (e.g. "/dummy")
|
||||
# 2. a URL relative to the current path
|
||||
# Note that any query string will be discarded.
|
||||
path = _urljoin(request.path_info, path)
|
||||
|
||||
# Set a 'path' member attribute so that code which traps this
|
||||
# error can have access to it.
|
||||
self.path = path
|
||||
|
||||
CherryPyException.__init__(self, path, self.query_string)
|
||||
|
||||
|
||||
class HTTPRedirect(CherryPyException):
|
||||
"""Exception raised when the request should be redirected.
|
||||
|
||||
The new URL must be passed as the first argument to the Exception,
|
||||
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed. If a URL is
|
||||
absolute, it will be used as-is. If it is relative, it is assumed
|
||||
to be relative to the current cherrypy.request.path_info.
|
||||
"""
|
||||
|
||||
def __init__(self, urls, status=None):
|
||||
import cherrypy
|
||||
request = cherrypy.request
|
||||
|
||||
if isinstance(urls, basestring):
|
||||
urls = [urls]
|
||||
|
||||
abs_urls = []
|
||||
for url in urls:
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a complete URL with host (e.g. "http://www.example.com/test")
|
||||
# 2. a URL relative to root (e.g. "/dummy")
|
||||
# 3. a URL relative to the current path
|
||||
# Note that any query string in cherrypy.request is discarded.
|
||||
url = _urljoin(cherrypy.url(), url)
|
||||
abs_urls.append(url)
|
||||
self.urls = abs_urls
|
||||
|
||||
# RFC 2616 indicates a 301 response code fits our goal; however,
|
||||
# browser support for 301 is quite messy. Do 302/303 instead. See
|
||||
# http://www.alanflavell.org.uk/www/post-redirect.html
|
||||
if status is None:
|
||||
if request.protocol >= (1, 1):
|
||||
status = 303
|
||||
else:
|
||||
status = 302
|
||||
else:
|
||||
status = int(status)
|
||||
if status < 300 or status > 399:
|
||||
raise ValueError("status must be between 300 and 399.")
|
||||
|
||||
self.status = status
|
||||
CherryPyException.__init__(self, abs_urls, status)
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPRedirect object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
response = cherrypy.response
|
||||
response.status = status = self.status
|
||||
|
||||
if status in (300, 301, 302, 303, 307):
|
||||
response.headers['Content-Type'] = "text/html"
|
||||
# "The ... URI SHOULD be given by the Location field
|
||||
# in the response."
|
||||
response.headers['Location'] = self.urls[0]
|
||||
|
||||
# "Unless the request method was HEAD, the entity of the response
|
||||
# SHOULD contain a short hypertext note with a hyperlink to the
|
||||
# new URI(s)."
|
||||
msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
|
||||
301: "This resource has permanently moved to <a href='%s'>%s</a>.",
|
||||
302: "This resource resides temporarily at <a href='%s'>%s</a>.",
|
||||
303: "This resource can be found at <a href='%s'>%s</a>.",
|
||||
307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
|
||||
}[status]
|
||||
response.body = "<br />\n".join([msg % (u, u) for u in self.urls])
|
||||
# Previous code may have set C-L, so we have to reset it
|
||||
# (allow finalize to set it).
|
||||
response.headers.pop('Content-Length', None)
|
||||
elif status == 304:
|
||||
# Not Modified.
|
||||
# "The response MUST include the following header fields:
|
||||
# Date, unless its omission is required by section 14.18.1"
|
||||
# The "Date" header should have been set in Response.__init__
|
||||
|
||||
# "...the response SHOULD NOT include other entity-headers."
|
||||
for key in ('Allow', 'Content-Encoding', 'Content-Language',
|
||||
'Content-Length', 'Content-Location', 'Content-MD5',
|
||||
'Content-Range', 'Content-Type', 'Expires',
|
||||
'Last-Modified'):
|
||||
if key in response.headers:
|
||||
del response.headers[key]
|
||||
|
||||
# "The 304 response MUST NOT contain a message-body."
|
||||
response.body = None
|
||||
# Previous code may have set C-L, so we have to reset it.
|
||||
response.headers.pop('Content-Length', None)
|
||||
elif status == 305:
|
||||
# Use Proxy.
|
||||
# self.urls[0] should be the URI of the proxy.
|
||||
response.headers['Location'] = self.urls[0]
|
||||
response.body = None
|
||||
# Previous code may have set C-L, so we have to reset it.
|
||||
response.headers.pop('Content-Length', None)
|
||||
else:
|
||||
raise ValueError("The %s status code is unknown." % status)
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
||||
|
||||
def clean_headers(status):
|
||||
"""Remove any headers which should not apply to an error response."""
|
||||
import cherrypy
|
||||
|
||||
response = cherrypy.response
|
||||
|
||||
# Remove headers which applied to the original content,
|
||||
# but do not apply to the error page.
|
||||
respheaders = response.headers
|
||||
for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
|
||||
"Vary", "Content-Encoding", "Content-Length", "Expires",
|
||||
"Content-Location", "Content-MD5", "Last-Modified"]:
|
||||
if respheaders.has_key(key):
|
||||
del respheaders[key]
|
||||
|
||||
if status != 416:
|
||||
# A server sending a response with status code 416 (Requested
|
||||
# range not satisfiable) SHOULD include a Content-Range field
|
||||
# with a byte-range-resp-spec of "*". The instance-length
|
||||
# specifies the current length of the selected resource.
|
||||
# A response with status code 206 (Partial Content) MUST NOT
|
||||
# include a Content-Range field with a byte-range- resp-spec of "*".
|
||||
if respheaders.has_key("Content-Range"):
|
||||
del respheaders["Content-Range"]
|
||||
|
||||
|
||||
class HTTPError(CherryPyException):
|
||||
""" Exception used to return an HTTP error code (4xx-5xx) to the client.
|
||||
This exception will automatically set the response status and body.
|
||||
|
||||
A custom message (a long description to display in the browser)
|
||||
can be provided in place of the default.
|
||||
"""
|
||||
|
||||
def __init__(self, status=500, message=None):
|
||||
self.status = status
|
||||
try:
|
||||
self.code, self.reason, defaultmsg = _http.valid_status(status)
|
||||
except ValueError, x:
|
||||
raise cherrypy.HTTPError(500, x.args[0])
|
||||
|
||||
if self.code < 400 or self.code > 599:
|
||||
raise ValueError("status must be between 400 and 599.")
|
||||
|
||||
# See http://www.python.org/dev/peps/pep-0352/
|
||||
# self.message = message
|
||||
self._message = message or defaultmsg
|
||||
CherryPyException.__init__(self, status, message)
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPError object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
response = cherrypy.response
|
||||
|
||||
clean_headers(self.code)
|
||||
|
||||
# In all cases, finalize will be called after this method,
|
||||
# so don't bother cleaning up response values here.
|
||||
response.status = self.status
|
||||
tb = None
|
||||
if cherrypy.request.show_tracebacks:
|
||||
tb = format_exc()
|
||||
response.headers['Content-Type'] = "text/html"
|
||||
|
||||
content = self.get_error_page(self.status, traceback=tb,
|
||||
message=self._message)
|
||||
response.body = content
|
||||
response.headers['Content-Length'] = len(content)
|
||||
|
||||
_be_ie_unfriendly(self.code)
|
||||
|
||||
def get_error_page(self, *args, **kwargs):
|
||||
return get_error_page(*args, **kwargs)
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
||||
|
||||
class NotFound(HTTPError):
|
||||
"""Exception raised when a URL could not be mapped to any handler (404)."""
|
||||
|
||||
def __init__(self, path=None):
|
||||
if path is None:
|
||||
import cherrypy
|
||||
path = cherrypy.request.script_name + cherrypy.request.path_info
|
||||
self.args = (path,)
|
||||
HTTPError.__init__(self, 404, "The path %r was not found." % path)
|
||||
|
||||
|
||||
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
|
||||
<title>%(status)s</title>
|
||||
<style type="text/css">
|
||||
#powered_by {
|
||||
margin-top: 20px;
|
||||
border-top: 2px solid black;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#traceback {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%(status)s</h2>
|
||||
<p>%(message)s</p>
|
||||
<pre id="traceback">%(traceback)s</pre>
|
||||
<div id="powered_by">
|
||||
<span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
def get_error_page(status, **kwargs):
|
||||
"""Return an HTML page, containing a pretty error response.
|
||||
|
||||
status should be an int or a str.
|
||||
kwargs will be interpolated into the page template.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
try:
|
||||
code, reason, message = _http.valid_status(status)
|
||||
except ValueError, x:
|
||||
raise cherrypy.HTTPError(500, x.args[0])
|
||||
|
||||
# We can't use setdefault here, because some
|
||||
# callers send None for kwarg values.
|
||||
if kwargs.get('status') is None:
|
||||
kwargs['status'] = "%s %s" % (code, reason)
|
||||
if kwargs.get('message') is None:
|
||||
kwargs['message'] = message
|
||||
if kwargs.get('traceback') is None:
|
||||
kwargs['traceback'] = ''
|
||||
if kwargs.get('version') is None:
|
||||
kwargs['version'] = cherrypy.__version__
|
||||
|
||||
for k, v in kwargs.iteritems():
|
||||
if v is None:
|
||||
kwargs[k] = ""
|
||||
else:
|
||||
kwargs[k] = _escape(kwargs[k])
|
||||
|
||||
# Use a custom template or callable for the error page?
|
||||
pages = cherrypy.request.error_page
|
||||
error_page = pages.get(code) or pages.get('default')
|
||||
if error_page:
|
||||
try:
|
||||
if callable(error_page):
|
||||
return error_page(**kwargs)
|
||||
else:
|
||||
return file(error_page, 'rb').read() % kwargs
|
||||
except:
|
||||
e = _format_exception(*_exc_info())[-1]
|
||||
m = kwargs['message']
|
||||
if m:
|
||||
m += "<br />"
|
||||
m += "In addition, the custom error page failed:\n<br />%s" % e
|
||||
kwargs['message'] = m
|
||||
|
||||
return _HTTPErrorTemplate % kwargs
|
||||
|
||||
|
||||
_ie_friendly_error_sizes = {
|
||||
400: 512, 403: 256, 404: 512, 405: 256,
|
||||
406: 512, 408: 512, 409: 512, 410: 256,
|
||||
500: 512, 501: 512, 505: 512,
|
||||
}
|
||||
|
||||
|
||||
def _be_ie_unfriendly(status):
|
||||
import cherrypy
|
||||
response = cherrypy.response
|
||||
|
||||
# For some statuses, Internet Explorer 5+ shows "friendly error
|
||||
# messages" instead of our response.body if the body is smaller
|
||||
# than a given size. Fix this by returning a body over that size
|
||||
# (by adding whitespace).
|
||||
# See http://support.microsoft.com/kb/q218155/
|
||||
s = _ie_friendly_error_sizes.get(status, 0)
|
||||
if s:
|
||||
s += 1
|
||||
# Since we are issuing an HTTP error status, we assume that
|
||||
# the entity is short, and we should just collapse it.
|
||||
content = response.collapse_body()
|
||||
l = len(content)
|
||||
if l and l < s:
|
||||
# IN ADDITION: the response must be written to IE
|
||||
# in one chunk or it will still get replaced! Bah.
|
||||
content = content + (" " * (s - l))
|
||||
response.body = content
|
||||
response.headers['Content-Length'] = len(content)
|
||||
|
||||
|
||||
def format_exc(exc=None):
|
||||
"""Return exc (or sys.exc_info if None), formatted."""
|
||||
if exc is None:
|
||||
exc = _exc_info()
|
||||
if exc == (None, None, None):
|
||||
return ""
|
||||
import traceback
|
||||
return "".join(traceback.format_exception(*exc))
|
||||
|
||||
def bare_error(extrabody=None):
|
||||
"""Produce status, headers, body for a critical error.
|
||||
|
||||
Returns a triple without calling any other questionable functions,
|
||||
so it should be as error-free as possible. Call it from an HTTP server
|
||||
if you get errors outside of the request.
|
||||
|
||||
If extrabody is None, a friendly but rather unhelpful error message
|
||||
is set in the body. If extrabody is a string, it will be appended
|
||||
as-is to the body.
|
||||
"""
|
||||
|
||||
# The whole point of this function is to be a last line-of-defense
|
||||
# in handling errors. That is, it must not raise any errors itself;
|
||||
# it cannot be allowed to fail. Therefore, don't add to it!
|
||||
# In particular, don't call any other CP functions.
|
||||
|
||||
body = "Unrecoverable error in the server."
|
||||
if extrabody is not None:
|
||||
body += "\n" + extrabody
|
||||
|
||||
return ("500 Internal Server Error",
|
||||
[('Content-Type', 'text/plain'),
|
||||
('Content-Length', str(len(body)))],
|
||||
[body])
|
||||
|
||||
|
||||
245
cherrypy/_cplogging.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""CherryPy logging."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
|
||||
logging.Logger.manager.emittedNoHandlerWarning = 1
|
||||
logfmt = logging.Formatter("%(message)s")
|
||||
import os
|
||||
import rfc822
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cperror
|
||||
|
||||
|
||||
class LogManager(object):
|
||||
|
||||
appid = None
|
||||
error_log = None
|
||||
access_log = None
|
||||
access_log_format = \
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
def __init__(self, appid=None, logger_root="cherrypy"):
|
||||
self.logger_root = logger_root
|
||||
self.appid = appid
|
||||
if appid is None:
|
||||
self.error_log = logging.getLogger("%s.error" % logger_root)
|
||||
self.access_log = logging.getLogger("%s.access" % logger_root)
|
||||
else:
|
||||
self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
|
||||
self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
|
||||
self.error_log.setLevel(logging.DEBUG)
|
||||
self.access_log.setLevel(logging.INFO)
|
||||
cherrypy.engine.subscribe('graceful', self.reopen_files)
|
||||
|
||||
def reopen_files(self):
|
||||
"""Close and reopen all file handlers."""
|
||||
for log in (self.error_log, self.access_log):
|
||||
for h in log.handlers:
|
||||
if isinstance(h, logging.FileHandler):
|
||||
h.acquire()
|
||||
h.stream.close()
|
||||
h.stream = open(h.baseFilename, h.mode)
|
||||
h.release()
|
||||
|
||||
def error(self, msg='', context='', severity=logging.INFO, traceback=False):
|
||||
"""Write to the error log.
|
||||
|
||||
This is not just for errors! Applications may call this at any time
|
||||
to log application-specific information.
|
||||
"""
|
||||
if traceback:
|
||||
msg += _cperror.format_exc()
|
||||
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Write to the error log.
|
||||
|
||||
This is not just for errors! Applications may call this at any time
|
||||
to log application-specific information.
|
||||
"""
|
||||
return self.error(*args, **kwargs)
|
||||
|
||||
def access(self):
|
||||
"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
|
||||
Like Apache started doing in 2.0.46, non-printable and other special
|
||||
characters in %r (and we expand that to all parts) are escaped using
|
||||
\\xhh sequences, where hh stands for the hexadecimal representation
|
||||
of the raw byte. Exceptions from this rule are " and \\, which are
|
||||
escaped by prepending a backslash, and all whitespace characters,
|
||||
which are written in their C-style notation (\\n, \\t, etc).
|
||||
"""
|
||||
request = cherrypy.request
|
||||
remote = request.remote
|
||||
response = cherrypy.response
|
||||
outheaders = response.headers
|
||||
inheaders = request.headers
|
||||
|
||||
atoms = {'h': remote.name or remote.ip,
|
||||
'l': '-',
|
||||
'u': getattr(request, "login", None) or "-",
|
||||
't': self.time(),
|
||||
'r': request.request_line,
|
||||
's': response.status.split(" ", 1)[0],
|
||||
'b': outheaders.get('Content-Length', '') or "-",
|
||||
'f': inheaders.get('Referer', ''),
|
||||
'a': inheaders.get('User-Agent', ''),
|
||||
}
|
||||
for k, v in atoms.items():
|
||||
if isinstance(v, unicode):
|
||||
v = v.encode('utf8')
|
||||
elif not isinstance(v, str):
|
||||
v = str(v)
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[1:-1]
|
||||
# Escape double-quote.
|
||||
atoms[k] = v.replace('"', '\\"')
|
||||
|
||||
try:
|
||||
self.access_log.log(logging.INFO, self.access_log_format % atoms)
|
||||
except:
|
||||
self(traceback=True)
|
||||
|
||||
def time(self):
|
||||
"""Return now() in Apache Common Log Format (no timezone)."""
|
||||
now = datetime.datetime.now()
|
||||
month = rfc822._monthnames[now.month - 1].capitalize()
|
||||
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
|
||||
(now.day, month, now.year, now.hour, now.minute, now.second))
|
||||
|
||||
def _get_builtin_handler(self, log, key):
|
||||
for h in log.handlers:
|
||||
if getattr(h, "_cpbuiltin", None) == key:
|
||||
return h
|
||||
|
||||
|
||||
# ------------------------- Screen handlers ------------------------- #
|
||||
|
||||
def _set_screen_handler(self, log, enable, stream=None):
|
||||
h = self._get_builtin_handler(log, "screen")
|
||||
if enable:
|
||||
if not h:
|
||||
if stream is None:
|
||||
stream=sys.stderr
|
||||
h = logging.StreamHandler(stream)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "screen"
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_screen(self):
|
||||
h = self._get_builtin_handler
|
||||
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
|
||||
return bool(has_h)
|
||||
|
||||
def _set_screen(self, newvalue):
|
||||
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
|
||||
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
|
||||
screen = property(_get_screen, _set_screen,
|
||||
doc="If True, error and access will print to stderr.")
|
||||
|
||||
|
||||
# -------------------------- File handlers -------------------------- #
|
||||
|
||||
def _add_builtin_file_handler(self, log, fname):
|
||||
h = logging.FileHandler(fname)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "file"
|
||||
log.addHandler(h)
|
||||
|
||||
def _set_file_handler(self, log, filename):
|
||||
h = self._get_builtin_handler(log, "file")
|
||||
if filename:
|
||||
if h:
|
||||
if h.baseFilename != os.path.abspath(filename):
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
self._add_builtin_file_handler(log, filename)
|
||||
else:
|
||||
self._add_builtin_file_handler(log, filename)
|
||||
else:
|
||||
if h:
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_error_file(self):
|
||||
h = self._get_builtin_handler(self.error_log, "file")
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
def _set_error_file(self, newvalue):
|
||||
self._set_file_handler(self.error_log, newvalue)
|
||||
error_file = property(_get_error_file, _set_error_file,
|
||||
doc="The filename for self.error_log.")
|
||||
|
||||
def _get_access_file(self):
|
||||
h = self._get_builtin_handler(self.access_log, "file")
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
def _set_access_file(self, newvalue):
|
||||
self._set_file_handler(self.access_log, newvalue)
|
||||
access_file = property(_get_access_file, _set_access_file,
|
||||
doc="The filename for self.access_log.")
|
||||
|
||||
|
||||
# ------------------------- WSGI handlers ------------------------- #
|
||||
|
||||
def _set_wsgi_handler(self, log, enable):
|
||||
h = self._get_builtin_handler(log, "wsgi")
|
||||
if enable:
|
||||
if not h:
|
||||
h = WSGIErrorHandler()
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "wsgi"
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_wsgi(self):
|
||||
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
|
||||
|
||||
def _set_wsgi(self, newvalue):
|
||||
self._set_wsgi_handler(self.error_log, newvalue)
|
||||
wsgi = property(_get_wsgi, _set_wsgi,
|
||||
doc="If True, error messages will be sent to wsgi.errors.")
|
||||
|
||||
|
||||
class WSGIErrorHandler(logging.Handler):
|
||||
"A handler class which writes logging records to environ['wsgi.errors']."
|
||||
|
||||
def flush(self):
|
||||
"""Flushes the stream."""
|
||||
try:
|
||||
stream = cherrypy.request.wsgi_environ.get('wsgi.errors')
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
else:
|
||||
stream.flush()
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a record."""
|
||||
try:
|
||||
stream = cherrypy.request.wsgi_environ.get('wsgi.errors')
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
msg = self.format(record)
|
||||
fs = "%s\n"
|
||||
import types
|
||||
if not hasattr(types, "UnicodeType"): #if no unicode support...
|
||||
stream.write(fs % msg)
|
||||
else:
|
||||
try:
|
||||
stream.write(fs % msg)
|
||||
except UnicodeError:
|
||||
stream.write(fs % msg.encode("UTF-8"))
|
||||
self.flush()
|
||||
except:
|
||||
self.handleError(record)
|
||||
330
cherrypy/_cpmodpy.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""Native adapter for serving CherryPy via mod_python
|
||||
|
||||
Basic usage:
|
||||
|
||||
##########################################
|
||||
# Application in a module called myapp.py
|
||||
##########################################
|
||||
|
||||
import cherrypy
|
||||
|
||||
class Root:
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hi there, Ho there, Hey there'
|
||||
|
||||
|
||||
# We will use this method from the mod_python configuration
|
||||
# as the entry point to our application
|
||||
def setup_server():
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({'environment': 'production',
|
||||
'log.screen': False,
|
||||
'show_tracebacks': False})
|
||||
|
||||
##########################################
|
||||
# mod_python settings for apache2
|
||||
# This should reside in your httpd.conf
|
||||
# or a file that will be loaded at
|
||||
# apache startup
|
||||
##########################################
|
||||
|
||||
# Start
|
||||
DocumentRoot "/"
|
||||
Listen 8080
|
||||
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
|
||||
|
||||
<Location "/">
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
SetHandler python-program
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup myapp::setup_server
|
||||
PythonDebug On
|
||||
</Location>
|
||||
# End
|
||||
|
||||
The actual path to your mod_python.so is dependent on your
|
||||
environment. In this case we suppose a global mod_python
|
||||
installation on a Linux distribution such as Ubuntu.
|
||||
|
||||
We do set the PythonPath configuration setting so that
|
||||
your application can be found by from the user running
|
||||
the apache2 instance. Of course if your application
|
||||
resides in the global site-package this won't be needed.
|
||||
|
||||
Then restart apache2 and access http://127.0.0.1:8080
|
||||
"""
|
||||
|
||||
import logging
|
||||
import StringIO
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import http
|
||||
|
||||
|
||||
|
||||
# ------------------------------ Request-handling
|
||||
|
||||
|
||||
|
||||
def setup(req):
|
||||
from mod_python import apache
|
||||
|
||||
# Run any setup function defined by a "PythonOption cherrypy.setup" directive.
|
||||
options = req.get_options()
|
||||
if 'cherrypy.setup' in options:
|
||||
atoms = options['cherrypy.setup'].split('::', 1)
|
||||
if len(atoms) == 1:
|
||||
mod = __import__(atoms[0], globals(), locals())
|
||||
else:
|
||||
modname, fname = atoms
|
||||
mod = __import__(modname, globals(), locals(), [fname])
|
||||
func = getattr(mod, fname)
|
||||
func()
|
||||
|
||||
cherrypy.config.update({'log.screen': False,
|
||||
"tools.ignore_headers.on": True,
|
||||
"tools.ignore_headers.headers": ['Range'],
|
||||
})
|
||||
|
||||
engine = cherrypy.engine
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.unsubscribe()
|
||||
if hasattr(engine, "console_control_handler"):
|
||||
engine.console_control_handler.unsubscribe()
|
||||
engine.autoreload.unsubscribe()
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
def _log(msg, level):
|
||||
newlevel = apache.APLOG_ERR
|
||||
if logging.DEBUG >= level:
|
||||
newlevel = apache.APLOG_DEBUG
|
||||
elif logging.INFO >= level:
|
||||
newlevel = apache.APLOG_INFO
|
||||
elif logging.WARNING >= level:
|
||||
newlevel = apache.APLOG_WARNING
|
||||
# On Windows, req.server is required or the msg will vanish. See
|
||||
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html.
|
||||
# Also, "When server is not specified...LogLevel does not apply..."
|
||||
apache.log_error(msg, newlevel, req.server)
|
||||
engine.subscribe('log', _log)
|
||||
|
||||
engine.start()
|
||||
|
||||
def cherrypy_cleanup(data):
|
||||
engine.exit()
|
||||
try:
|
||||
# apache.register_cleanup wasn't available until 3.1.4.
|
||||
apache.register_cleanup(cherrypy_cleanup)
|
||||
except AttributeError:
|
||||
req.server.register_cleanup(req, cherrypy_cleanup)
|
||||
|
||||
|
||||
class _ReadOnlyRequest:
|
||||
expose = ('read', 'readline', 'readlines')
|
||||
def __init__(self, req):
|
||||
for method in self.expose:
|
||||
self.__dict__[method] = getattr(req, method)
|
||||
|
||||
|
||||
recursive = False
|
||||
|
||||
_isSetUp = False
|
||||
def handler(req):
|
||||
from mod_python import apache
|
||||
try:
|
||||
global _isSetUp
|
||||
if not _isSetUp:
|
||||
setup(req)
|
||||
_isSetUp = True
|
||||
|
||||
# Obtain a Request object from CherryPy
|
||||
local = req.connection.local_addr
|
||||
local = http.Host(local[0], local[1], req.connection.local_host or "")
|
||||
remote = req.connection.remote_addr
|
||||
remote = http.Host(remote[0], remote[1], req.connection.remote_host or "")
|
||||
|
||||
scheme = req.parsed_uri[0] or 'http'
|
||||
req.get_basic_auth_pw()
|
||||
|
||||
try:
|
||||
# apache.mpm_query only became available in mod_python 3.1
|
||||
q = apache.mpm_query
|
||||
threaded = q(apache.AP_MPMQ_IS_THREADED)
|
||||
forked = q(apache.AP_MPMQ_IS_FORKED)
|
||||
except AttributeError:
|
||||
bad_value = ("You must provide a PythonOption '%s', "
|
||||
"either 'on' or 'off', when running a version "
|
||||
"of mod_python < 3.1")
|
||||
|
||||
threaded = options.get('multithread', '').lower()
|
||||
if threaded == 'on':
|
||||
threaded = True
|
||||
elif threaded == 'off':
|
||||
threaded = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multithread")
|
||||
|
||||
forked = options.get('multiprocess', '').lower()
|
||||
if forked == 'on':
|
||||
forked = True
|
||||
elif forked == 'off':
|
||||
forked = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multiprocess")
|
||||
|
||||
sn = cherrypy.tree.script_name(req.uri or "/")
|
||||
if sn is None:
|
||||
send_response(req, '404 Not Found', [], '')
|
||||
else:
|
||||
app = cherrypy.tree.apps[sn]
|
||||
method = req.method
|
||||
path = req.uri
|
||||
qs = req.args or ""
|
||||
reqproto = req.protocol
|
||||
headers = req.headers_in.items()
|
||||
rfile = _ReadOnlyRequest(req)
|
||||
prev = None
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
request, response = app.get_serving(local, remote, scheme,
|
||||
"HTTP/1.1")
|
||||
request.login = req.user
|
||||
request.multithread = bool(threaded)
|
||||
request.multiprocess = bool(forked)
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
try:
|
||||
request.run(method, path, qs, reqproto, headers, rfile)
|
||||
break
|
||||
except cherrypy.InternalRedirect, ir:
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
if not recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
"same URL twice: %r" % ir.path)
|
||||
else:
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
# Munge environment and try again.
|
||||
method = "GET"
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = StringIO.StringIO()
|
||||
|
||||
send_response(req, response.status, response.header_list,
|
||||
response.body, response.stream)
|
||||
finally:
|
||||
app.release_serving()
|
||||
except:
|
||||
tb = format_exc()
|
||||
cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
|
||||
s, h, b = bare_error()
|
||||
send_response(req, s, h, b)
|
||||
return apache.OK
|
||||
|
||||
|
||||
def send_response(req, status, headers, body, stream=False):
|
||||
# Set response status
|
||||
req.status = int(status[:3])
|
||||
|
||||
# Set response headers
|
||||
req.content_type = "text/plain"
|
||||
for header, value in headers:
|
||||
if header.lower() == 'content-type':
|
||||
req.content_type = value
|
||||
continue
|
||||
req.headers_out.add(header, value)
|
||||
|
||||
if stream:
|
||||
# Flush now so the status and headers are sent immediately.
|
||||
req.flush()
|
||||
|
||||
# Set response body
|
||||
if isinstance(body, basestring):
|
||||
req.write(body)
|
||||
else:
|
||||
for seg in body:
|
||||
req.write(seg)
|
||||
|
||||
|
||||
|
||||
# --------------- Startup tools for CherryPy + mod_python --------------- #
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def read_process(cmd, args=""):
|
||||
pipein, pipeout = os.popen4("%s %s" % (cmd, args))
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(r"(not recognized|No such file|not found)", firstline,
|
||||
re.IGNORECASE)):
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
class ModPythonServer(object):
|
||||
|
||||
template = """
|
||||
# Apache2 server configuration file for running CherryPy with mod_python.
|
||||
|
||||
DocumentRoot "/"
|
||||
Listen %(port)s
|
||||
LoadModule python_module modules/mod_python.so
|
||||
|
||||
<Location %(loc)s>
|
||||
SetHandler python-program
|
||||
PythonHandler %(handler)s
|
||||
PythonDebug On
|
||||
%(opts)s
|
||||
</Location>
|
||||
"""
|
||||
|
||||
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
|
||||
handler="cherrypy._cpmodpy::handler"):
|
||||
self.loc = loc
|
||||
self.port = port
|
||||
self.opts = opts
|
||||
self.apache_path = apache_path
|
||||
self.handler = handler
|
||||
|
||||
def start(self):
|
||||
opts = "".join([" PythonOption %s %s\n" % (k, v)
|
||||
for k, v in self.opts])
|
||||
conf_data = self.template % {"port": self.port,
|
||||
"loc": self.loc,
|
||||
"opts": opts,
|
||||
"handler": self.handler,
|
||||
}
|
||||
|
||||
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
f.write(conf_data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
|
||||
self.ready = True
|
||||
return response
|
||||
|
||||
def stop(self):
|
||||
os.popen("apache -k stop")
|
||||
self.ready = False
|
||||
|
||||
912
cherrypy/_cprequest.py
Normal file
@@ -0,0 +1,912 @@
|
||||
|
||||
import Cookie
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cpcgifs, _cpconfig
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import http, file_generator
|
||||
|
||||
|
||||
class Hook(object):
|
||||
"""A callback and its metadata: failsafe, priority, and kwargs."""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
callback = None
|
||||
callback__doc = """
|
||||
The bare callable that this Hook object is wrapping, which will
|
||||
be called when the Hook is called."""
|
||||
|
||||
failsafe = False
|
||||
failsafe__doc = """
|
||||
If True, the callback is guaranteed to run even if other callbacks
|
||||
from the same call point raise exceptions."""
|
||||
|
||||
priority = 50
|
||||
priority__doc = """
|
||||
Defines the order of execution for a list of Hooks. Priority numbers
|
||||
should be limited to the closed interval [0, 100], but values outside
|
||||
this range are acceptable, as are fractional values."""
|
||||
|
||||
kwargs = {}
|
||||
kwargs__doc = """
|
||||
A set of keyword arguments that will be passed to the
|
||||
callable on each call."""
|
||||
|
||||
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
|
||||
self.callback = callback
|
||||
|
||||
if failsafe is None:
|
||||
failsafe = getattr(callback, "failsafe", False)
|
||||
self.failsafe = failsafe
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, "priority", 50)
|
||||
self.priority = priority
|
||||
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.priority, other.priority)
|
||||
|
||||
def __call__(self):
|
||||
"""Run self.callback(**self.kwargs)."""
|
||||
return self.callback(**self.kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
|
||||
% (cls.__module__, cls.__name__, self.callback,
|
||||
self.failsafe, self.priority,
|
||||
", ".join(['%s=%r' % (k, v)
|
||||
for k, v in self.kwargs.iteritems()])))
|
||||
|
||||
|
||||
class HookMap(dict):
|
||||
"""A map of call points to lists of callbacks (Hook objects)."""
|
||||
|
||||
def __new__(cls, points=None):
|
||||
d = dict.__new__(cls)
|
||||
for p in points or []:
|
||||
d[p] = []
|
||||
return d
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
|
||||
"""Append a new Hook made from the supplied arguments."""
|
||||
self[point].append(Hook(callback, failsafe, priority, **kwargs))
|
||||
|
||||
def run(self, point):
|
||||
"""Execute all registered Hooks (callbacks) for the given point."""
|
||||
exc = None
|
||||
hooks = self[point]
|
||||
hooks.sort()
|
||||
for hook in hooks:
|
||||
# Some hooks are guaranteed to run even if others at
|
||||
# the same hookpoint fail. We will still log the failure,
|
||||
# but proceed on to the next hook. The only way
|
||||
# to stop all processing from one of these hooks is
|
||||
# to raise SystemExit and stop the whole server.
|
||||
if exc is None or hook.failsafe:
|
||||
try:
|
||||
hook()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
|
||||
cherrypy.InternalRedirect):
|
||||
exc = sys.exc_info()[1]
|
||||
except:
|
||||
exc = sys.exc_info()[1]
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if exc:
|
||||
raise
|
||||
|
||||
def __copy__(self):
|
||||
newmap = self.__class__()
|
||||
# We can't just use 'update' because we want copies of the
|
||||
# mutable values (each is a list) as well.
|
||||
for k, v in self.iteritems():
|
||||
newmap[k] = v[:]
|
||||
return newmap
|
||||
copy = __copy__
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, self.keys())
|
||||
|
||||
|
||||
# Config namespace handlers
|
||||
|
||||
def hooks_namespace(k, v):
|
||||
"""Attach bare hooks declared in config."""
|
||||
# Use split again to allow multiple hooks for a single
|
||||
# hookpoint per path (e.g. "hooks.before_handler.1").
|
||||
# Little-known fact you only get from reading source ;)
|
||||
hookpoint = k.split(".", 1)[0]
|
||||
if isinstance(v, basestring):
|
||||
v = cherrypy.lib.attributes(v)
|
||||
if not isinstance(v, Hook):
|
||||
v = Hook(v)
|
||||
cherrypy.request.hooks[hookpoint].append(v)
|
||||
|
||||
def request_namespace(k, v):
|
||||
"""Attach request attributes declared in config."""
|
||||
setattr(cherrypy.request, k, v)
|
||||
|
||||
def response_namespace(k, v):
|
||||
"""Attach response attributes declared in config."""
|
||||
setattr(cherrypy.response, k, v)
|
||||
|
||||
def error_page_namespace(k, v):
|
||||
"""Attach error pages declared in config."""
|
||||
if k != 'default':
|
||||
k = int(k)
|
||||
cherrypy.request.error_page[k] = v
|
||||
|
||||
|
||||
hookpoints = ['on_start_resource', 'before_request_body',
|
||||
'before_handler', 'before_finalize',
|
||||
'on_end_resource', 'on_end_request',
|
||||
'before_error_response', 'after_error_response']
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""An HTTP request.
|
||||
|
||||
This object represents the metadata of an HTTP request message;
|
||||
that is, it contains attributes which describe the environment
|
||||
in which the request URL, headers, and body were sent (if you
|
||||
want tools to interpret the headers and body, those are elsewhere,
|
||||
mostly in Tools). This 'metadata' consists of socket data,
|
||||
transport characteristics, and the Request-Line. This object
|
||||
also contains data regarding the configuration in effect for
|
||||
the given URL, and the execution plan for generating a response.
|
||||
"""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
prev = None
|
||||
prev__doc = """
|
||||
The previous Request object (if any). This should be None
|
||||
unless we are processing an InternalRedirect."""
|
||||
|
||||
# Conversation/connection attributes
|
||||
local = http.Host("127.0.0.1", 80)
|
||||
local__doc = \
|
||||
"An http.Host(ip, port, hostname) object for the server socket."
|
||||
|
||||
remote = http.Host("127.0.0.1", 1111)
|
||||
remote__doc = \
|
||||
"An http.Host(ip, port, hostname) object for the client socket."
|
||||
|
||||
scheme = "http"
|
||||
scheme__doc = """
|
||||
The protocol used between client and server. In most cases,
|
||||
this will be either 'http' or 'https'."""
|
||||
|
||||
server_protocol = "HTTP/1.1"
|
||||
server_protocol__doc = """
|
||||
The HTTP version for which the HTTP server is at least
|
||||
conditionally compliant."""
|
||||
|
||||
base = ""
|
||||
base__doc = """The (scheme://host) portion of the requested URL.
|
||||
In some cases (e.g. when proxying via mod_rewrite), this may contain
|
||||
path segments which cherrypy.url uses when constructing url's, but
|
||||
which otherwise are ignored by CherryPy. Regardless, this value
|
||||
MUST NOT end in a slash."""
|
||||
|
||||
# Request-Line attributes
|
||||
request_line = ""
|
||||
request_line__doc = """
|
||||
The complete Request-Line received from the client. This is a
|
||||
single string consisting of the request method, URI, and protocol
|
||||
version (joined by spaces). Any final CRLF is removed."""
|
||||
|
||||
method = "GET"
|
||||
method__doc = """
|
||||
Indicates the HTTP method to be performed on the resource identified
|
||||
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
|
||||
DELETE. CherryPy allows any extension method; however, various HTTP
|
||||
servers and gateways may restrict the set of allowable methods.
|
||||
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
|
||||
|
||||
query_string = ""
|
||||
query_string__doc = """
|
||||
The query component of the Request-URI, a string of information to be
|
||||
interpreted by the resource. The query portion of a URI follows the
|
||||
path component, and is separated by a '?'. For example, the URI
|
||||
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
|
||||
'a=3&b=4'."""
|
||||
|
||||
protocol = (1, 1)
|
||||
protocol__doc = """The HTTP protocol version corresponding to the set
|
||||
of features which should be allowed in the response. If BOTH
|
||||
the client's request message AND the server's level of HTTP
|
||||
compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
|
||||
If either is 1.0, this attribute will be the tuple (1, 0).
|
||||
Lower HTTP protocol versions are not explicitly supported."""
|
||||
|
||||
params = {}
|
||||
params__doc = """
|
||||
A dict which combines query string (GET) and request entity (POST)
|
||||
variables. This is populated in two stages: GET params are added
|
||||
before the 'on_start_resource' hook, and POST params are added
|
||||
between the 'before_request_body' and 'before_handler' hooks."""
|
||||
|
||||
# Message attributes
|
||||
header_list = []
|
||||
header_list__doc = """
|
||||
A list of the HTTP request headers as (name, value) tuples.
|
||||
In general, you should use request.headers (a dict) instead."""
|
||||
|
||||
headers = http.HeaderMap()
|
||||
headers__doc = """
|
||||
A dict-like object containing the request headers. Keys are header
|
||||
names (in Title-Case format); however, you may get and set them in
|
||||
a case-insensitive manner. That is, headers['Content-Type'] and
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to RFC 2047 if necessary). See also:
|
||||
http.HeaderMap, http.HeaderElement."""
|
||||
|
||||
cookie = Cookie.SimpleCookie()
|
||||
cookie__doc = """See help(Cookie)."""
|
||||
|
||||
rfile = None
|
||||
rfile__doc = """
|
||||
If the request included an entity (body), it will be available
|
||||
as a stream in this attribute. However, the rfile will normally
|
||||
be read for you between the 'before_request_body' hook and the
|
||||
'before_handler' hook, and the resulting string is placed into
|
||||
either request.params or the request.body attribute.
|
||||
|
||||
You may disable the automatic consumption of the rfile by setting
|
||||
request.process_request_body to False, either in config for the desired
|
||||
path, or in an 'on_start_resource' or 'before_request_body' hook.
|
||||
|
||||
WARNING: In almost every case, you should not attempt to read from the
|
||||
rfile stream after CherryPy's automatic mechanism has read it. If you
|
||||
turn off the automatic parsing of rfile, you should read exactly the
|
||||
number of bytes specified in request.headers['Content-Length'].
|
||||
Ignoring either of these warnings may result in a hung request thread
|
||||
or in corruption of the next (pipelined) request.
|
||||
"""
|
||||
|
||||
process_request_body = True
|
||||
process_request_body__doc = """
|
||||
If True, the rfile (if any) is automatically read and parsed,
|
||||
and the result placed into request.params or request.body."""
|
||||
|
||||
methods_with_bodies = ("POST", "PUT")
|
||||
methods_with_bodies__doc = """
|
||||
A sequence of HTTP methods for which CherryPy will automatically
|
||||
attempt to read a body from the rfile."""
|
||||
|
||||
body = None
|
||||
body__doc = """
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded'
|
||||
or multipart, this will be None. Otherwise, this will contain the
|
||||
request entity body as a string; this value is set between the
|
||||
'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True)."""
|
||||
|
||||
body_params = None
|
||||
body_params__doc = """
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded' or
|
||||
multipart, this will be a dict of the params pulled from the entity
|
||||
body; that is, it will be the portion of request.params that come
|
||||
from the message body (sometimes called "POST params", although they
|
||||
can be sent with various HTTP method verbs). This value is set between
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True)."""
|
||||
|
||||
# Dispatch attributes
|
||||
dispatch = cherrypy.dispatch.Dispatcher()
|
||||
dispatch__doc = """
|
||||
The object which looks up the 'page handler' callable and collects
|
||||
config for the current request based on the path_info, other
|
||||
request attributes, and the application architecture. The core
|
||||
calls the dispatcher as early as possible, passing it a 'path_info'
|
||||
argument.
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
See help(cherrypy.dispatch) for more information."""
|
||||
|
||||
script_name = ""
|
||||
script_name__doc = """
|
||||
The 'mount point' of the application which is handling this request.
|
||||
|
||||
This attribute MUST NOT end in a slash. If the script_name refers to
|
||||
the root of the URI, it MUST be an empty string (not "/").
|
||||
"""
|
||||
|
||||
path_info = "/"
|
||||
path_info__doc = """
|
||||
The 'relative path' portion of the Request-URI. This is relative
|
||||
to the script_name ('mount point') of the application which is
|
||||
handling this request."""
|
||||
|
||||
login = None
|
||||
login__doc = """
|
||||
When authentication is used during the request processing this is
|
||||
set to 'False' if it failed and to the 'username' value if it succeeded.
|
||||
The default 'None' implies that no authentication happened."""
|
||||
|
||||
# Note that cherrypy.url uses "if request.app:" to determine whether
|
||||
# the call is during a real HTTP request or not. So leave this None.
|
||||
app = None
|
||||
app__doc = \
|
||||
"""The cherrypy.Application object which is handling this request."""
|
||||
|
||||
handler = None
|
||||
handler__doc = """
|
||||
The function, method, or other callable which CherryPy will call to
|
||||
produce the response. The discovery of the handler and the arguments
|
||||
it will receive are determined by the request.dispatch object.
|
||||
By default, the handler is discovered by walking a tree of objects
|
||||
starting at request.app.root, and is then passed all HTTP params
|
||||
(from the query string and POST body) as keyword arguments."""
|
||||
|
||||
toolmaps = {}
|
||||
toolmaps__doc = """
|
||||
A nested dict of all Toolboxes and Tools in effect for this request,
|
||||
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
|
||||
|
||||
config = None
|
||||
config__doc = """
|
||||
A flat dict of all configuration entries which apply to the
|
||||
current request. These entries are collected from global config,
|
||||
application config (based on request.path_info), and from handler
|
||||
config (exactly how is governed by the request.dispatch object in
|
||||
effect for this request; by default, handler config can be attached
|
||||
anywhere in the tree between request.app.root and the final handler,
|
||||
and inherits downward)."""
|
||||
|
||||
is_index = None
|
||||
is_index__doc = """
|
||||
This will be True if the current request is mapped to an 'index'
|
||||
resource handler (also, a 'default' handler if path_info ends with
|
||||
a slash). The value may be used to automatically redirect the
|
||||
user-agent to a 'more canonical' URL which either adds or removes
|
||||
the trailing slash. See cherrypy.tools.trailing_slash."""
|
||||
|
||||
hooks = HookMap(hookpoints)
|
||||
hooks__doc = """
|
||||
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
|
||||
Each key is a str naming the hook point, and each value is a list
|
||||
of hooks which will be called at that hook point during this request.
|
||||
The list of hooks is generally populated as early as possible (mostly
|
||||
from Tools specified in config), but may be extended at any time.
|
||||
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
|
||||
|
||||
error_response = cherrypy.HTTPError(500).set_response
|
||||
error_response__doc = """
|
||||
The no-arg callable which will handle unexpected, untrapped errors
|
||||
during request processing. This is not used for expected exceptions
|
||||
(like NotFound, HTTPError, or HTTPRedirect) which are raised in
|
||||
response to expected conditions (those should be customized either
|
||||
via request.error_page or by overriding HTTPError.set_response).
|
||||
By default, error_response uses HTTPError(500) to return a generic
|
||||
error response to the user-agent."""
|
||||
|
||||
error_page = {}
|
||||
error_page__doc = """
|
||||
A dict of {error code: response filename or callable} pairs.
|
||||
|
||||
The error code must be an int representing a given HTTP error code,
|
||||
or the string 'default', which will be used if no matching entry
|
||||
is found for a given numeric code.
|
||||
|
||||
If a filename is provided, the file should contain a Python string-
|
||||
formatting template, and can expect by default to receive format
|
||||
values with the mapping keys %(status)s, %(message)s, %(traceback)s,
|
||||
and %(version)s. The set of format mappings can be extended by
|
||||
overriding HTTPError.set_response.
|
||||
|
||||
If a callable is provided, it will be called by default with keyword
|
||||
arguments 'status', 'message', 'traceback', and 'version', as for a
|
||||
string-formatting template. The callable must return a string which
|
||||
will be set to response.body. It may also override headers or perform
|
||||
any other processing.
|
||||
|
||||
If no entry is given for an error code, and no 'default' entry exists,
|
||||
a default template will be used.
|
||||
"""
|
||||
|
||||
show_tracebacks = True
|
||||
show_tracebacks__doc = """
|
||||
If True, unexpected errors encountered during request processing will
|
||||
include a traceback in the response body."""
|
||||
|
||||
show_mismatched_params = True
|
||||
show_mismatched_params__doc = """
|
||||
If True, mismatched parameters encountered during PageHandler invocation
|
||||
processing will be included in the response body."""
|
||||
|
||||
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
|
||||
throws__doc = \
|
||||
"""The sequence of exceptions which Request.run does not trap."""
|
||||
|
||||
throw_errors = False
|
||||
throw_errors__doc = """
|
||||
If True, Request.run will not trap any errors (except HTTPRedirect and
|
||||
HTTPError, which are more properly called 'exceptions', not errors)."""
|
||||
|
||||
closed = False
|
||||
closed__doc = """
|
||||
True once the close method has been called, False otherwise."""
|
||||
|
||||
stage = None
|
||||
stage__doc = """
|
||||
A string containing the stage reached in the request-handling process.
|
||||
This is useful when debugging a live server with hung requests."""
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet(
|
||||
**{"hooks": hooks_namespace,
|
||||
"request": request_namespace,
|
||||
"response": response_namespace,
|
||||
"error_page": error_page_namespace,
|
||||
"tools": cherrypy.tools,
|
||||
})
|
||||
|
||||
def __init__(self, local_host, remote_host, scheme="http",
|
||||
server_protocol="HTTP/1.1"):
|
||||
"""Populate a new Request object.
|
||||
|
||||
local_host should be an http.Host object with the server info.
|
||||
remote_host should be an http.Host object with the client info.
|
||||
scheme should be a string, either "http" or "https".
|
||||
"""
|
||||
self.local = local_host
|
||||
self.remote = remote_host
|
||||
self.scheme = scheme
|
||||
self.server_protocol = server_protocol
|
||||
|
||||
self.closed = False
|
||||
|
||||
# Put a *copy* of the class error_page into self.
|
||||
self.error_page = self.error_page.copy()
|
||||
|
||||
# Put a *copy* of the class namespaces into self.
|
||||
self.namespaces = self.namespaces.copy()
|
||||
|
||||
self.stage = None
|
||||
|
||||
def close(self):
|
||||
"""Run cleanup code. (Core)"""
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
self.stage = 'on_end_request'
|
||||
self.hooks.run('on_end_request')
|
||||
self.stage = 'close'
|
||||
|
||||
def run(self, method, path, query_string, req_protocol, headers, rfile):
|
||||
"""Process the Request. (Core)
|
||||
|
||||
method, path, query_string, and req_protocol should be pulled directly
|
||||
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
|
||||
path should be %XX-unquoted, but query_string should not be.
|
||||
headers should be a list of (name, value) tuples.
|
||||
rfile should be a file-like object containing the HTTP request entity.
|
||||
|
||||
When run() is done, the returned object should have 3 attributes:
|
||||
status, e.g. "200 OK"
|
||||
header_list, a list of (name, value) tuples
|
||||
body, an iterable yielding strings
|
||||
|
||||
Consumer code (HTTP servers) should then access these response
|
||||
attributes to build the outbound stream.
|
||||
|
||||
"""
|
||||
self.stage = 'run'
|
||||
try:
|
||||
self.error_response = cherrypy.HTTPError(500).set_response
|
||||
|
||||
self.method = method
|
||||
path = path or "/"
|
||||
self.query_string = query_string or ''
|
||||
|
||||
# Compare request and server HTTP protocol versions, in case our
|
||||
# server does not support the requested protocol. Limit our output
|
||||
# to min(req, server). We want the following output:
|
||||
# request server actual written supported response
|
||||
# protocol protocol response protocol feature set
|
||||
# a 1.0 1.0 1.0 1.0
|
||||
# b 1.0 1.1 1.1 1.0
|
||||
# c 1.1 1.0 1.0 1.0
|
||||
# d 1.1 1.1 1.1 1.1
|
||||
# Notice that, in (b), the response will be "HTTP/1.1" even though
|
||||
# the client only understands 1.0. RFC 2616 10.5.6 says we should
|
||||
# only return 505 if the _major_ version is different.
|
||||
rp = int(req_protocol[5]), int(req_protocol[7])
|
||||
sp = int(self.server_protocol[5]), int(self.server_protocol[7])
|
||||
self.protocol = min(rp, sp)
|
||||
|
||||
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
|
||||
url = path
|
||||
if query_string:
|
||||
url += '?' + query_string
|
||||
self.request_line = '%s %s %s' % (method, url, req_protocol)
|
||||
|
||||
self.header_list = list(headers)
|
||||
self.rfile = rfile
|
||||
self.headers = http.HeaderMap()
|
||||
self.cookie = Cookie.SimpleCookie()
|
||||
self.handler = None
|
||||
|
||||
# path_info should be the path from the
|
||||
# app root (script_name) to the handler.
|
||||
self.script_name = self.app.script_name
|
||||
self.path_info = pi = path[len(self.script_name):]
|
||||
|
||||
self.stage = 'respond'
|
||||
self.respond(pi)
|
||||
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
if self.throw_errors:
|
||||
raise
|
||||
else:
|
||||
# Failure in setup, error handler or finalize. Bypass them.
|
||||
# Can't use handle_error because we may not have hooks yet.
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if self.show_tracebacks:
|
||||
body = format_exc()
|
||||
else:
|
||||
body = ""
|
||||
r = bare_error(body)
|
||||
response = cherrypy.response
|
||||
response.status, response.header_list, response.body = r
|
||||
|
||||
if self.method == "HEAD":
|
||||
# HEAD requests MUST NOT return a message-body in the response.
|
||||
cherrypy.response.body = []
|
||||
|
||||
try:
|
||||
cherrypy.log.access()
|
||||
except:
|
||||
cherrypy.log.error(traceback=True)
|
||||
|
||||
if cherrypy.response.timed_out:
|
||||
raise cherrypy.TimeoutError()
|
||||
|
||||
return cherrypy.response
|
||||
|
||||
def respond(self, path_info):
|
||||
"""Generate a response for the resource at self.path_info. (Core)"""
|
||||
try:
|
||||
try:
|
||||
try:
|
||||
if self.app is None:
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Get the 'Host' header, so we can HTTPRedirect properly.
|
||||
self.stage = 'process_headers'
|
||||
self.process_headers()
|
||||
|
||||
# Make a copy of the class hooks
|
||||
self.hooks = self.__class__.hooks.copy()
|
||||
self.toolmaps = {}
|
||||
self.stage = 'get_resource'
|
||||
self.get_resource(path_info)
|
||||
self.namespaces(self.config)
|
||||
|
||||
self.stage = 'on_start_resource'
|
||||
self.hooks.run('on_start_resource')
|
||||
|
||||
if self.process_request_body:
|
||||
if self.method not in self.methods_with_bodies:
|
||||
self.process_request_body = False
|
||||
|
||||
self.stage = 'before_request_body'
|
||||
self.hooks.run('before_request_body')
|
||||
if self.process_request_body:
|
||||
self.process_body()
|
||||
|
||||
self.stage = 'before_handler'
|
||||
self.hooks.run('before_handler')
|
||||
if self.handler:
|
||||
self.stage = 'handler'
|
||||
cherrypy.response.body = self.handler()
|
||||
|
||||
self.stage = 'before_finalize'
|
||||
self.hooks.run('before_finalize')
|
||||
cherrypy.response.finalize()
|
||||
except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
|
||||
inst.set_response()
|
||||
self.stage = 'before_finalize (HTTPError)'
|
||||
self.hooks.run('before_finalize')
|
||||
cherrypy.response.finalize()
|
||||
finally:
|
||||
self.stage = 'on_end_resource'
|
||||
self.hooks.run('on_end_resource')
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
if self.throw_errors:
|
||||
raise
|
||||
self.handle_error()
|
||||
|
||||
def process_headers(self):
|
||||
"""Parse HTTP header data into Python structures. (Core)"""
|
||||
self.params = http.parse_query_string(self.query_string)
|
||||
|
||||
# Process the headers into self.headers
|
||||
headers = self.headers
|
||||
for name, value in self.header_list:
|
||||
# Call title() now (and use dict.__method__(headers))
|
||||
# so title doesn't have to be called twice.
|
||||
name = name.title()
|
||||
value = value.strip()
|
||||
|
||||
# Warning: if there is more than one header entry for cookies (AFAIK,
|
||||
# only Konqueror does that), only the last one will remain in headers
|
||||
# (but they will be correctly stored in request.cookie).
|
||||
if "=?" in value:
|
||||
dict.__setitem__(headers, name, http.decode_TEXT(value))
|
||||
else:
|
||||
dict.__setitem__(headers, name, value)
|
||||
|
||||
# Handle cookies differently because on Konqueror, multiple
|
||||
# cookies come on different lines with the same key
|
||||
if name == 'Cookie':
|
||||
self.cookie.load(value)
|
||||
|
||||
if not dict.__contains__(headers, 'Host'):
|
||||
# All Internet-based HTTP/1.1 servers MUST respond with a 400
|
||||
# (Bad Request) status code to any HTTP/1.1 request message
|
||||
# which lacks a Host header field.
|
||||
if self.protocol >= (1, 1):
|
||||
msg = "HTTP/1.1 requires a 'Host' request header."
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
host = dict.get(headers, 'Host')
|
||||
if not host:
|
||||
host = self.local.name or self.local.ip
|
||||
self.base = "%s://%s" % (self.scheme, host)
|
||||
|
||||
def get_resource(self, path):
|
||||
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
|
||||
dispatch = self.dispatch
|
||||
# First, see if there is a custom dispatch at this URI. Custom
|
||||
# dispatchers can only be specified in app.config, not in _cp_config
|
||||
# (since custom dispatchers may not even have an app.root).
|
||||
trail = path or "/"
|
||||
while trail:
|
||||
nodeconf = self.app.config.get(trail, {})
|
||||
|
||||
d = nodeconf.get("request.dispatch")
|
||||
if d:
|
||||
dispatch = d
|
||||
break
|
||||
|
||||
lastslash = trail.rfind("/")
|
||||
if lastslash == -1:
|
||||
break
|
||||
elif lastslash == 0 and trail != "/":
|
||||
trail = "/"
|
||||
else:
|
||||
trail = trail[:lastslash]
|
||||
|
||||
# dispatch() should set self.handler and self.config
|
||||
dispatch(path)
|
||||
|
||||
def process_body(self):
|
||||
"""Convert request.rfile into request.params (or request.body). (Core)"""
|
||||
if not self.headers.get("Content-Length", ""):
|
||||
# No Content-Length header supplied (or it's 0).
|
||||
# If we went ahead and called cgi.FieldStorage, it would hang,
|
||||
# since it cannot determine when to stop reading from the socket.
|
||||
# See http://www.cherrypy.org/ticket/493.
|
||||
# See also http://www.cherrypy.org/ticket/650.
|
||||
# Note also that we expect any HTTP server to have decoded
|
||||
# any message-body that had a transfer-coding, and we expect
|
||||
# the HTTP server to have supplied a Content-Length header
|
||||
# which is valid for the decoded entity-body.
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
# If the headers are missing "Content-Type" then add one
|
||||
# with an empty value. This ensures that FieldStorage
|
||||
# won't parse the request body for params if the client
|
||||
# didn't provide a "Content-Type" header.
|
||||
if 'Content-Type' not in self.headers:
|
||||
h = http.HeaderMap(self.headers.items())
|
||||
h['Content-Type'] = ''
|
||||
else:
|
||||
h = self.headers
|
||||
|
||||
try:
|
||||
forms = _cpcgifs.FieldStorage(fp=self.rfile,
|
||||
headers=h,
|
||||
# FieldStorage only recognizes POST.
|
||||
environ={'REQUEST_METHOD': "POST"},
|
||||
keep_blank_values=1)
|
||||
except Exception, e:
|
||||
if e.__class__.__name__ == 'MaxSizeExceeded':
|
||||
# Post data is too big
|
||||
raise cherrypy.HTTPError(413)
|
||||
else:
|
||||
raise
|
||||
|
||||
# Note that, if headers['Content-Type'] is multipart/*,
|
||||
# then forms.file will not exist; instead, each form[key]
|
||||
# item will be its own file object, and will be handled
|
||||
# by params_from_CGI_form.
|
||||
if forms.file:
|
||||
# request body was a content-type other than multipart.
|
||||
self.body = forms.file
|
||||
else:
|
||||
self.body_params = p = http.params_from_CGI_form(forms)
|
||||
self.params.update(p)
|
||||
|
||||
def handle_error(self):
|
||||
"""Handle the last unanticipated exception. (Core)"""
|
||||
try:
|
||||
self.hooks.run("before_error_response")
|
||||
if self.error_response:
|
||||
self.error_response()
|
||||
self.hooks.run("after_error_response")
|
||||
cherrypy.response.finalize()
|
||||
except cherrypy.HTTPRedirect, inst:
|
||||
inst.set_response()
|
||||
cherrypy.response.finalize()
|
||||
|
||||
|
||||
class Body(object):
|
||||
"""The body of the HTTP response (the response entity)."""
|
||||
|
||||
def __get__(self, obj, objclass=None):
|
||||
if obj is None:
|
||||
# When calling on the class instead of an instance...
|
||||
return self
|
||||
else:
|
||||
return obj._body
|
||||
|
||||
def __set__(self, obj, value):
|
||||
# Convert the given value to an iterable object.
|
||||
if isinstance(value, basestring):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# item list is much faster than iterating over every character
|
||||
# in a long string.
|
||||
if value:
|
||||
value = [value]
|
||||
else:
|
||||
# [''] doesn't evaluate to False, so replace it with [].
|
||||
value = []
|
||||
elif isinstance(value, types.FileType):
|
||||
value = file_generator(value)
|
||||
elif value is None:
|
||||
value = []
|
||||
obj._body = value
|
||||
|
||||
|
||||
class Response(object):
|
||||
"""An HTTP Response, including status, headers, and body.
|
||||
|
||||
Application developers should use Response.headers (a dict) to
|
||||
set or modify HTTP response headers. When the response is finalized,
|
||||
Response.headers is transformed into Response.header_list as
|
||||
(key, value) tuples.
|
||||
"""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
# Class attributes for dev-time introspection.
|
||||
status = ""
|
||||
status__doc = """The HTTP Status-Code and Reason-Phrase."""
|
||||
|
||||
header_list = []
|
||||
header_list__doc = """
|
||||
A list of the HTTP response headers as (name, value) tuples.
|
||||
In general, you should use response.headers (a dict) instead."""
|
||||
|
||||
headers = http.HeaderMap()
|
||||
headers__doc = """
|
||||
A dict-like object containing the response headers. Keys are header
|
||||
names (in Title-Case format); however, you may get and set them in
|
||||
a case-insensitive manner. That is, headers['Content-Type'] and
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to RFC 2047 if necessary). See also:
|
||||
http.HeaderMap, http.HeaderElement."""
|
||||
|
||||
cookie = Cookie.SimpleCookie()
|
||||
cookie__doc = """See help(Cookie)."""
|
||||
|
||||
body = Body()
|
||||
body__doc = """The body (entity) of the HTTP response."""
|
||||
|
||||
time = None
|
||||
time__doc = """The value of time.time() when created. Use in HTTP dates."""
|
||||
|
||||
timeout = 300
|
||||
timeout__doc = """Seconds after which the response will be aborted."""
|
||||
|
||||
timed_out = False
|
||||
timed_out__doc = """
|
||||
Flag to indicate the response should be aborted, because it has
|
||||
exceeded its timeout."""
|
||||
|
||||
stream = False
|
||||
stream__doc = """If False, buffer the response body."""
|
||||
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.header_list = None
|
||||
self._body = []
|
||||
self.time = time.time()
|
||||
|
||||
self.headers = http.HeaderMap()
|
||||
# Since we know all our keys are titled strings, we can
|
||||
# bypass HeaderMap.update and get a big speed boost.
|
||||
dict.update(self.headers, {
|
||||
"Content-Type": 'text/html',
|
||||
"Server": "CherryPy/" + cherrypy.__version__,
|
||||
"Date": http.HTTPDate(self.time),
|
||||
})
|
||||
self.cookie = Cookie.SimpleCookie()
|
||||
|
||||
def collapse_body(self):
|
||||
"""Collapse self.body to a single string; replace it and return it."""
|
||||
newbody = ''.join([chunk for chunk in self.body])
|
||||
self.body = newbody
|
||||
return newbody
|
||||
|
||||
def finalize(self):
|
||||
"""Transform headers (and cookies) into self.header_list. (Core)"""
|
||||
try:
|
||||
code, reason, _ = http.valid_status(self.status)
|
||||
except ValueError, x:
|
||||
raise cherrypy.HTTPError(500, x.args[0])
|
||||
|
||||
self.status = "%s %s" % (code, reason)
|
||||
|
||||
headers = self.headers
|
||||
if self.stream:
|
||||
# The upshot: wsgiserver will chunk the response if
|
||||
# you pop Content-Length (or set it explicitly to None).
|
||||
# Note that lib.static sets C-L to the file's st_size.
|
||||
if dict.get(headers, 'Content-Length') is None:
|
||||
dict.pop(headers, 'Content-Length', None)
|
||||
elif code < 200 or code in (204, 205, 304):
|
||||
# "All 1xx (informational), 204 (no content),
|
||||
# and 304 (not modified) responses MUST NOT
|
||||
# include a message-body."
|
||||
dict.pop(headers, 'Content-Length', None)
|
||||
self.body = ""
|
||||
else:
|
||||
# Responses which are not streamed should have a Content-Length,
|
||||
# but allow user code to set Content-Length if desired.
|
||||
if dict.get(headers, 'Content-Length') is None:
|
||||
content = self.collapse_body()
|
||||
dict.__setitem__(headers, 'Content-Length', len(content))
|
||||
|
||||
# Transform our header dict into a list of tuples.
|
||||
self.header_list = h = headers.output(cherrypy.request.protocol)
|
||||
|
||||
cookie = self.cookie.output()
|
||||
if cookie:
|
||||
for line in cookie.split("\n"):
|
||||
if line.endswith("\r"):
|
||||
# Python 2.4 emits cookies joined by LF but 2.5+ by CRLF.
|
||||
line = line[:-1]
|
||||
name, value = line.split(": ", 1)
|
||||
h.append((name, value))
|
||||
|
||||
def check_timeout(self):
|
||||
"""If now > self.time + self.timeout, set self.timed_out.
|
||||
|
||||
This purposefully sets a flag, rather than raising an error,
|
||||
so that a monitor thread can interrupt the Response thread.
|
||||
"""
|
||||
if time.time() > self.time + self.timeout:
|
||||
self.timed_out = True
|
||||
|
||||
|
||||
|
||||
143
cherrypy/_cpserver.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Manage HTTP servers with CherryPy."""
|
||||
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import attributes
|
||||
|
||||
# We import * because we want to export check_port
|
||||
# et al as attributes of this module.
|
||||
from cherrypy.process.servers import *
|
||||
|
||||
|
||||
class Server(ServerAdapter):
|
||||
"""An adapter for an HTTP server.
|
||||
|
||||
You can set attributes (like socket_host and socket_port)
|
||||
on *this* object (which is probably cherrypy.server), and call
|
||||
quickstart. For example:
|
||||
|
||||
cherrypy.server.socket_port = 80
|
||||
cherrypy.quickstart()
|
||||
"""
|
||||
|
||||
socket_port = 8080
|
||||
|
||||
_socket_host = '127.0.0.1'
|
||||
def _get_socket_host(self):
|
||||
return self._socket_host
|
||||
def _set_socket_host(self, value):
|
||||
if value == '':
|
||||
raise ValueError("The empty string ('') is not an allowed value. "
|
||||
"Use '0.0.0.0' instead to listen on all active "
|
||||
"interfaces (INADDR_ANY).")
|
||||
self._socket_host = value
|
||||
socket_host = property(_get_socket_host, _set_socket_host,
|
||||
doc="""The hostname or IP address on which to listen for connections.
|
||||
|
||||
Host values may be any IPv4 or IPv6 address, or any valid hostname.
|
||||
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
|
||||
your hosts file prefers IPv6). The string '0.0.0.0' is a special
|
||||
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
|
||||
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
|
||||
not allowed.""")
|
||||
|
||||
socket_file = None
|
||||
socket_queue_size = 5
|
||||
socket_timeout = 10
|
||||
shutdown_timeout = 5
|
||||
protocol_version = 'HTTP/1.1'
|
||||
reverse_dns = False
|
||||
thread_pool = 10
|
||||
thread_pool_max = -1
|
||||
max_request_header_size = 500 * 1024
|
||||
max_request_body_size = 100 * 1024 * 1024
|
||||
instance = None
|
||||
ssl_context = None
|
||||
ssl_certificate = None
|
||||
ssl_certificate_chain = None
|
||||
ssl_private_key = None
|
||||
nodelay = True
|
||||
|
||||
def __init__(self):
|
||||
self.bus = cherrypy.engine
|
||||
self.httpserver = None
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
def quickstart(self, server=None):
|
||||
"""This does nothing now and will be removed in 3.2."""
|
||||
warnings.warn('quickstart does nothing now and will be removed in '
|
||||
'3.2. Call cherrypy.engine.start() instead.',
|
||||
DeprecationWarning)
|
||||
|
||||
def httpserver_from_self(self, httpserver=None):
|
||||
"""Return a (httpserver, bind_addr) pair based on self attributes."""
|
||||
if httpserver is None:
|
||||
httpserver = self.instance
|
||||
if httpserver is None:
|
||||
from cherrypy import _cpwsgi_server
|
||||
httpserver = _cpwsgi_server.CPWSGIServer(self)
|
||||
if isinstance(httpserver, basestring):
|
||||
# Is anyone using this? Can I add an arg?
|
||||
httpserver = attributes(httpserver)(self)
|
||||
return httpserver, self.bind_addr
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if not self.httpserver:
|
||||
self.httpserver, self.bind_addr = self.httpserver_from_self()
|
||||
ServerAdapter.start(self)
|
||||
start.priority = 75
|
||||
|
||||
def _get_bind_addr(self):
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
if self.socket_host is None and self.socket_port is None:
|
||||
return None
|
||||
return (self.socket_host, self.socket_port)
|
||||
def _set_bind_addr(self, value):
|
||||
if value is None:
|
||||
self.socket_file = None
|
||||
self.socket_host = None
|
||||
self.socket_port = None
|
||||
elif isinstance(value, basestring):
|
||||
self.socket_file = value
|
||||
self.socket_host = None
|
||||
self.socket_port = None
|
||||
else:
|
||||
try:
|
||||
self.socket_host, self.socket_port = value
|
||||
self.socket_file = None
|
||||
except ValueError:
|
||||
raise ValueError("bind_addr must be a (host, port) tuple "
|
||||
"(for TCP sockets) or a string (for Unix "
|
||||
"domain sockets), not %r" % value)
|
||||
bind_addr = property(_get_bind_addr, _set_bind_addr)
|
||||
|
||||
def base(self):
|
||||
"""Return the base (scheme://host[:port] or sock file) for this server."""
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
|
||||
host = self.socket_host
|
||||
if host in ('0.0.0.0', '::'):
|
||||
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
|
||||
# Look up the host name, which should be the
|
||||
# safest thing to spit out in a URL.
|
||||
import socket
|
||||
host = socket.gethostname()
|
||||
|
||||
port = self.socket_port
|
||||
|
||||
if self.ssl_certificate:
|
||||
scheme = "https"
|
||||
if port != 443:
|
||||
host += ":%s" % port
|
||||
else:
|
||||
scheme = "http"
|
||||
if port != 80:
|
||||
host += ":%s" % port
|
||||
|
||||
return "%s://%s" % (scheme, host)
|
||||
|
||||
239
cherrypy/_cpthreadinglocal.py
Normal file
@@ -0,0 +1,239 @@
|
||||
# This is a backport of Python-2.4's threading.local() implementation
|
||||
|
||||
"""Thread-local objects
|
||||
|
||||
(Note that this module provides a Python version of thread
|
||||
threading.local class. Depending on the version of Python you're
|
||||
using, there may be a faster one available. You should always import
|
||||
the local class from threading.)
|
||||
|
||||
Thread-local objects support the management of thread-local data.
|
||||
If you have data that you want to be local to a thread, simply create
|
||||
a thread-local object and use its attributes:
|
||||
|
||||
>>> mydata = local()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
You can also access the local-object's dictionary:
|
||||
|
||||
>>> mydata.__dict__
|
||||
{'number': 42}
|
||||
>>> mydata.__dict__.setdefault('widgets', [])
|
||||
[]
|
||||
>>> mydata.widgets
|
||||
[]
|
||||
|
||||
What's important about thread-local objects is that their data are
|
||||
local to a thread. If we access the data in a different thread:
|
||||
|
||||
>>> log = []
|
||||
>>> def f():
|
||||
... items = mydata.__dict__.items()
|
||||
... items.sort()
|
||||
... log.append(items)
|
||||
... mydata.number = 11
|
||||
... log.append(mydata.number)
|
||||
|
||||
>>> import threading
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[], 11]
|
||||
|
||||
we get different data. Furthermore, changes made in the other thread
|
||||
don't affect data seen in this thread:
|
||||
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
Of course, values you get from a local object, including a __dict__
|
||||
attribute, are for whatever thread was current at the time the
|
||||
attribute was read. For that reason, you generally don't want to save
|
||||
these values across threads, as they apply only to the thread they
|
||||
came from.
|
||||
|
||||
You can create custom local objects by subclassing the local class:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... number = 2
|
||||
... initialized = False
|
||||
... def __init__(self, **kw):
|
||||
... if self.initialized:
|
||||
... raise SystemError('__init__ called too many times')
|
||||
... self.initialized = True
|
||||
... self.__dict__.update(kw)
|
||||
... def squared(self):
|
||||
... return self.number ** 2
|
||||
|
||||
This can be useful to support default values, methods and
|
||||
initialization. Note that if you define an __init__ method, it will be
|
||||
called each time the local object is used in a separate thread. This
|
||||
is necessary to initialize each thread's dictionary.
|
||||
|
||||
Now if we create a local object:
|
||||
|
||||
>>> mydata = MyLocal(color='red')
|
||||
|
||||
Now we have a default number:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
|
||||
an initial color:
|
||||
|
||||
>>> mydata.color
|
||||
'red'
|
||||
>>> del mydata.color
|
||||
|
||||
And a method that operates on the data:
|
||||
|
||||
>>> mydata.squared()
|
||||
4
|
||||
|
||||
As before, we can access the data in a separate thread:
|
||||
|
||||
>>> log = []
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[('color', 'red'), ('initialized', True)], 11]
|
||||
|
||||
without affecting this thread's data:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
>>> mydata.color
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'MyLocal' object has no attribute 'color'
|
||||
|
||||
Note that subclasses can define slots, but they are not thread
|
||||
local. They are shared across threads:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... __slots__ = 'number'
|
||||
|
||||
>>> mydata = MyLocal()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.color = 'red'
|
||||
|
||||
So, the separate thread:
|
||||
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
|
||||
affects what we see:
|
||||
|
||||
>>> mydata.number
|
||||
11
|
||||
|
||||
>>> del mydata
|
||||
"""
|
||||
|
||||
# Threading import is at end
|
||||
|
||||
class _localbase(object):
|
||||
__slots__ = '_local__key', '_local__args', '_local__lock'
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
self = object.__new__(cls)
|
||||
key = 'thread.local.' + str(id(self))
|
||||
object.__setattr__(self, '_local__key', key)
|
||||
object.__setattr__(self, '_local__args', (args, kw))
|
||||
object.__setattr__(self, '_local__lock', RLock())
|
||||
|
||||
if args or kw and (cls.__init__ is object.__init__):
|
||||
raise TypeError("Initialization arguments are not supported")
|
||||
|
||||
# We need to create the thread dict in anticipation of
|
||||
# __init__ being called, to make sure we don't call it
|
||||
# again ourselves.
|
||||
dict = object.__getattribute__(self, '__dict__')
|
||||
currentThread().__dict__[key] = dict
|
||||
|
||||
return self
|
||||
|
||||
def _patch(self):
|
||||
key = object.__getattribute__(self, '_local__key')
|
||||
d = currentThread().__dict__.get(key)
|
||||
if d is None:
|
||||
d = {}
|
||||
currentThread().__dict__[key] = d
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
# we have a new instance dict, so call out __init__ if we have
|
||||
# one
|
||||
cls = type(self)
|
||||
if cls.__init__ is not object.__init__:
|
||||
args, kw = object.__getattribute__(self, '_local__args')
|
||||
cls.__init__(self, *args, **kw)
|
||||
else:
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
class local(_localbase):
|
||||
|
||||
def __getattribute__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__getattribute__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__setattr__(self, name, value)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __delattr__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__delattr__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
|
||||
def __del__():
|
||||
threading_enumerate = enumerate
|
||||
__getattribute__ = object.__getattribute__
|
||||
|
||||
def __del__(self):
|
||||
key = __getattribute__(self, '_local__key')
|
||||
|
||||
try:
|
||||
threads = list(threading_enumerate())
|
||||
except:
|
||||
# if enumerate fails, as it seems to do during
|
||||
# shutdown, we'll skip cleanup under the assumption
|
||||
# that there is nothing to clean up
|
||||
return
|
||||
|
||||
for thread in threads:
|
||||
try:
|
||||
__dict__ = thread.__dict__
|
||||
except AttributeError:
|
||||
# Thread is dying, rest in peace
|
||||
continue
|
||||
|
||||
if key in __dict__:
|
||||
try:
|
||||
del __dict__[key]
|
||||
except KeyError:
|
||||
pass # didn't have anything in this thread
|
||||
|
||||
return __del__
|
||||
__del__ = __del__()
|
||||
|
||||
from threading import currentThread, enumerate, RLock
|
||||
492
cherrypy/_cptools.py
Normal file
@@ -0,0 +1,492 @@
|
||||
"""CherryPy tools. A "tool" is any helper, adapted to CP.
|
||||
|
||||
Tools are usually designed to be used in a variety of ways (although some
|
||||
may only offer one if they choose):
|
||||
|
||||
Library calls:
|
||||
All tools are callables that can be used wherever needed.
|
||||
The arguments are straightforward and should be detailed within the
|
||||
docstring.
|
||||
|
||||
Function decorators:
|
||||
All tools, when called, may be used as decorators which configure
|
||||
individual CherryPy page handlers (methods on the CherryPy tree).
|
||||
That is, "@tools.anytool()" should "turn on" the tool via the
|
||||
decorated function's _cp_config attribute.
|
||||
|
||||
CherryPy config:
|
||||
If a tool exposes a "_setup" callable, it will be called
|
||||
once per Request (if the feature is "turned on" via config).
|
||||
|
||||
Tools may be implemented as any object with a namespace. The builtins
|
||||
are generally either modules or instances of the tools.Tool class.
|
||||
"""
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
def _getargs(func):
|
||||
"""Return the names of all static arguments to the given function."""
|
||||
# Use this instead of importing inspect for less mem overhead.
|
||||
import types
|
||||
if isinstance(func, types.MethodType):
|
||||
func = func.im_func
|
||||
co = func.func_code
|
||||
return co.co_varnames[:co.co_argcount]
|
||||
|
||||
|
||||
class Tool(object):
|
||||
"""A registered function for use with CherryPy request-processing hooks.
|
||||
|
||||
help(tool.callable) should give you more information about this Tool.
|
||||
"""
|
||||
|
||||
namespace = "tools"
|
||||
|
||||
def __init__(self, point, callable, name=None, priority=50):
|
||||
self._point = point
|
||||
self.callable = callable
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
self.__doc__ = self.callable.__doc__
|
||||
self._setargs()
|
||||
|
||||
def _setargs(self):
|
||||
"""Copy func parameter names to obj attributes."""
|
||||
try:
|
||||
for arg in _getargs(self.callable):
|
||||
setattr(self, arg, None)
|
||||
except (TypeError, AttributeError):
|
||||
if hasattr(self.callable, "__call__"):
|
||||
for arg in _getargs(self.callable.__call__):
|
||||
setattr(self, arg, None)
|
||||
# IronPython 1.0 raises NotImplementedError because
|
||||
# inspect.getargspec tries to access Python bytecode
|
||||
# in co_code attribute.
|
||||
except NotImplementedError:
|
||||
pass
|
||||
# IronPython 1B1 may raise IndexError in some cases,
|
||||
# but if we trap it here it doesn't prevent CP from working.
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _merged_args(self, d=None):
|
||||
"""Return a dict of configuration entries for this Tool."""
|
||||
if d:
|
||||
conf = d.copy()
|
||||
else:
|
||||
conf = {}
|
||||
|
||||
tm = cherrypy.request.toolmaps[self.namespace]
|
||||
if self._name in tm:
|
||||
conf.update(tm[self._name])
|
||||
|
||||
if "on" in conf:
|
||||
del conf["on"]
|
||||
|
||||
return conf
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Compile-time decorator (turn on the tool in config).
|
||||
|
||||
For example:
|
||||
|
||||
@tools.proxy()
|
||||
def whats_my_base(self):
|
||||
return cherrypy.request.base
|
||||
whats_my_base.exposed = True
|
||||
"""
|
||||
if args:
|
||||
raise TypeError("The %r Tool does not accept positional "
|
||||
"arguments; you must use keyword arguments."
|
||||
% self._name)
|
||||
def tool_decorator(f):
|
||||
if not hasattr(f, "_cp_config"):
|
||||
f._cp_config = {}
|
||||
subspace = self.namespace + "." + self._name + "."
|
||||
f._cp_config[subspace + "on"] = True
|
||||
for k, v in kwargs.iteritems():
|
||||
f._cp_config[subspace + k] = v
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
conf = self._merged_args()
|
||||
p = conf.pop("priority", None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, "priority", self._priority)
|
||||
cherrypy.request.hooks.attach(self._point, self.callable,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
class HandlerTool(Tool):
|
||||
"""Tool which is called 'before main', that may skip normal handlers.
|
||||
|
||||
If the tool successfully handles the request (by setting response.body),
|
||||
if should return True. This will cause CherryPy to skip any 'normal' page
|
||||
handler. If the tool did not handle the request, it should return False
|
||||
to tell CherryPy to continue on and call the normal page handler. If the
|
||||
tool is declared AS a page handler (see the 'handler' method), returning
|
||||
False will raise NotFound.
|
||||
"""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, 'before_handler', callable, name)
|
||||
|
||||
def handler(self, *args, **kwargs):
|
||||
"""Use this tool as a CherryPy page handler.
|
||||
|
||||
For example:
|
||||
class Root:
|
||||
nav = tools.staticdir.handler(section="/nav", dir="nav",
|
||||
root=absDir)
|
||||
"""
|
||||
def handle_func(*a, **kw):
|
||||
handled = self.callable(*args, **self._merged_args(kwargs))
|
||||
if not handled:
|
||||
raise cherrypy.NotFound()
|
||||
return cherrypy.response.body
|
||||
handle_func.exposed = True
|
||||
return handle_func
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
if self.callable(**kwargs):
|
||||
cherrypy.request.handler = None
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
conf = self._merged_args()
|
||||
p = conf.pop("priority", None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, "priority", self._priority)
|
||||
cherrypy.request.hooks.attach(self._point, self._wrapper,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
class HandlerWrapperTool(Tool):
|
||||
"""Tool which wraps request.handler in a provided wrapper function.
|
||||
|
||||
The 'newhandler' arg must be a handler wrapper function that takes a
|
||||
'next_handler' argument, plus *args and **kwargs. Like all page handler
|
||||
functions, it must return an iterable for use as cherrypy.response.body.
|
||||
|
||||
For example, to allow your 'inner' page handlers to return dicts
|
||||
which then get interpolated into a template:
|
||||
|
||||
def interpolator(next_handler, *args, **kwargs):
|
||||
filename = cherrypy.request.config.get('template')
|
||||
cherrypy.response.template = env.get_template(filename)
|
||||
response_dict = next_handler(*args, **kwargs)
|
||||
return cherrypy.response.template.render(**response_dict)
|
||||
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
|
||||
"""
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None, priority=50):
|
||||
self.newhandler = newhandler
|
||||
self._point = point
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
|
||||
def callable(self):
|
||||
innerfunc = cherrypy.request.handler
|
||||
def wrap(*args, **kwargs):
|
||||
return self.newhandler(innerfunc, *args, **kwargs)
|
||||
cherrypy.request.handler = wrap
|
||||
|
||||
|
||||
class ErrorTool(Tool):
|
||||
"""Tool which is used to replace the default request.error_response."""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, None, callable, name)
|
||||
|
||||
def _wrapper(self):
|
||||
self.callable(**self._merged_args())
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
cherrypy.request.error_response = self._wrapper
|
||||
|
||||
|
||||
# Builtin tools #
|
||||
|
||||
from cherrypy.lib import cptools, encoding, auth, static, tidy
|
||||
from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
|
||||
from cherrypy.lib import caching as _caching, wsgiapp as _wsgiapp
|
||||
|
||||
|
||||
class SessionTool(Tool):
|
||||
"""Session Tool for CherryPy.
|
||||
|
||||
sessions.locking:
|
||||
When 'implicit' (the default), the session will be locked for you,
|
||||
just before running the page handler.
|
||||
When 'early', the session will be locked before reading the request
|
||||
body. This is off by default for safety reasons; for example,
|
||||
a large upload would block the session, denying an AJAX
|
||||
progress meter (see http://www.cherrypy.org/ticket/630).
|
||||
When 'explicit' (or any other value), you need to call
|
||||
cherrypy.session.acquire_lock() yourself before using
|
||||
session data.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# _sessions.init must be bound after headers are read
|
||||
Tool.__init__(self, 'before_request_body', _sessions.init)
|
||||
|
||||
def _lock_session(self):
|
||||
cherrypy.serving.session.acquire_lock()
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
hooks = cherrypy.request.hooks
|
||||
|
||||
conf = self._merged_args()
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, "priority", self._priority)
|
||||
|
||||
hooks.attach(self._point, self.callable, priority=p, **conf)
|
||||
|
||||
locking = conf.pop('locking', 'implicit')
|
||||
if locking == 'implicit':
|
||||
hooks.attach('before_handler', self._lock_session)
|
||||
elif locking == 'early':
|
||||
# Lock before the request body (but after _sessions.init runs!)
|
||||
hooks.attach('before_request_body', self._lock_session,
|
||||
priority=60)
|
||||
else:
|
||||
# Don't lock
|
||||
pass
|
||||
|
||||
hooks.attach('before_finalize', _sessions.save)
|
||||
hooks.attach('on_end_request', _sessions.close)
|
||||
|
||||
def regenerate(self):
|
||||
"""Drop the current session and make a new one (with a new id)."""
|
||||
sess = cherrypy.serving.session
|
||||
sess.regenerate()
|
||||
|
||||
# Grab cookie-relevant tool args
|
||||
conf = dict([(k, v) for k, v in self._merged_args().iteritems()
|
||||
if k in ('path', 'path_header', 'name', 'timeout',
|
||||
'domain', 'secure')])
|
||||
_sessions.set_response_cookie(**conf)
|
||||
|
||||
|
||||
|
||||
|
||||
class XMLRPCController(object):
|
||||
"""A Controller (page handler collection) for XML-RPC.
|
||||
|
||||
To use it, have your controllers subclass this base class (it will
|
||||
turn on the tool for you).
|
||||
|
||||
You can also supply the following optional config entries:
|
||||
|
||||
tools.xmlrpc.encoding: 'utf-8'
|
||||
tools.xmlrpc.allow_none: 0
|
||||
|
||||
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
|
||||
appropriate handler must first be performed according to the URL, and
|
||||
then a second dispatch step must take place according to the RPC method
|
||||
specified in the request body. It also allows a superfluous "/RPC2"
|
||||
prefix in the URL, supplies its own handler args in the body, and
|
||||
requires a 200 OK "Fault" response instead of 404 when the desired
|
||||
method is not found.
|
||||
|
||||
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
|
||||
This Controller acts as the dispatch target for the first half (based
|
||||
on the URL); it then reads the RPC method from the request body and
|
||||
does its own second dispatch step based on that method. It also reads
|
||||
body params, and returns a Fault on error.
|
||||
|
||||
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
|
||||
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
|
||||
Otherwise, you need to use declare it in config:
|
||||
|
||||
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
|
||||
"""
|
||||
|
||||
# Note we're hard-coding this into the 'tools' namespace. We could do
|
||||
# a huge amount of work to make it relocatable, but the only reason why
|
||||
# would be if someone actually disabled the default_toolbox. Meh.
|
||||
_cp_config = {'tools.xmlrpc.on': True}
|
||||
|
||||
def default(self, *vpath, **params):
|
||||
rpcparams, rpcmethod = _xmlrpc.process_body()
|
||||
|
||||
subhandler = self
|
||||
for attr in str(rpcmethod).split('.'):
|
||||
subhandler = getattr(subhandler, attr, None)
|
||||
|
||||
if subhandler and getattr(subhandler, "exposed", False):
|
||||
body = subhandler(*(vpath + rpcparams), **params)
|
||||
|
||||
else:
|
||||
# http://www.cherrypy.org/ticket/533
|
||||
# if a method is not found, an xmlrpclib.Fault should be returned
|
||||
# raising an exception here will do that; see
|
||||
# cherrypy.lib.xmlrpc.on_error
|
||||
raise Exception, 'method "%s" is not supported' % attr
|
||||
|
||||
conf = cherrypy.request.toolmaps['tools'].get("xmlrpc", {})
|
||||
_xmlrpc.respond(body,
|
||||
conf.get('encoding', 'utf-8'),
|
||||
conf.get('allow_none', 0))
|
||||
return cherrypy.response.body
|
||||
default.exposed = True
|
||||
|
||||
|
||||
class WSGIAppTool(HandlerTool):
|
||||
"""A tool for running any WSGI middleware/application within CP.
|
||||
|
||||
Here are the parameters:
|
||||
|
||||
wsgi_app - any wsgi application callable
|
||||
env_update - a dictionary with arbitrary keys and values to be
|
||||
merged with the WSGI environ dictionary.
|
||||
|
||||
Example:
|
||||
|
||||
class Whatever:
|
||||
_cp_config = {'tools.wsgiapp.on': True,
|
||||
'tools.wsgiapp.app': some_app,
|
||||
'tools.wsgiapp.env': app_environ,
|
||||
}
|
||||
"""
|
||||
|
||||
def _setup(self):
|
||||
# Keep request body intact so the wsgi app can have its way with it.
|
||||
cherrypy.request.process_request_body = False
|
||||
HandlerTool._setup(self)
|
||||
|
||||
|
||||
class SessionAuthTool(HandlerTool):
|
||||
|
||||
def _setargs(self):
|
||||
for name in dir(cptools.SessionAuth):
|
||||
if not name.startswith("__"):
|
||||
setattr(self, name, None)
|
||||
|
||||
|
||||
class CachingTool(Tool):
|
||||
"""Caching Tool for CherryPy."""
|
||||
|
||||
def _wrapper(self, invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
|
||||
request = cherrypy.request
|
||||
|
||||
if not hasattr(cherrypy, "_cache"):
|
||||
# Make a process-wide Cache object.
|
||||
cherrypy._cache = kwargs.pop("cache_class", _caching.MemoryCache)()
|
||||
|
||||
# Take all remaining kwargs and set them on the Cache object.
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(cherrypy._cache, k, v)
|
||||
|
||||
if _caching.get(invalid_methods=invalid_methods):
|
||||
request.handler = None
|
||||
else:
|
||||
if request.cacheable:
|
||||
# Note the devious technique here of adding hooks on the fly
|
||||
request.hooks.attach('before_finalize', _caching.tee_output,
|
||||
priority = 90)
|
||||
_wrapper.priority = 20
|
||||
|
||||
def _setup(self):
|
||||
"""Hook caching into cherrypy.request."""
|
||||
conf = self._merged_args()
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
cherrypy.request.hooks.attach('before_handler', self._wrapper,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
|
||||
class Toolbox(object):
|
||||
"""A collection of Tools.
|
||||
|
||||
This object also functions as a config namespace handler for itself.
|
||||
Custom toolboxes should be added to each Application's toolboxes dict.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# If the Tool._name is None, supply it from the attribute name.
|
||||
if isinstance(value, Tool):
|
||||
if value._name is None:
|
||||
value._name = name
|
||||
value.namespace = self.namespace
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def __enter__(self):
|
||||
"""Populate request.toolmaps from tools specified in config."""
|
||||
cherrypy.request.toolmaps[self.namespace] = map = {}
|
||||
def populate(k, v):
|
||||
toolname, arg = k.split(".", 1)
|
||||
bucket = map.setdefault(toolname, {})
|
||||
bucket[arg] = v
|
||||
return populate
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Run tool._setup() for each tool in our toolmap."""
|
||||
map = cherrypy.request.toolmaps.get(self.namespace)
|
||||
if map:
|
||||
for name, settings in map.items():
|
||||
if settings.get("on", False):
|
||||
tool = getattr(self, name)
|
||||
tool._setup()
|
||||
|
||||
|
||||
default_toolbox = _d = Toolbox("tools")
|
||||
_d.session_auth = SessionAuthTool(cptools.session_auth)
|
||||
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
|
||||
_d.response_headers = Tool('on_start_resource', cptools.response_headers)
|
||||
_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
|
||||
_d.log_headers = Tool('before_error_response', cptools.log_request_headers)
|
||||
_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
|
||||
_d.err_redirect = ErrorTool(cptools.redirect)
|
||||
_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
|
||||
_d.decode = Tool('before_handler', encoding.decode)
|
||||
# the order of encoding, gzip, caching is important
|
||||
_d.encode = Tool('before_finalize', encoding.encode, priority=70)
|
||||
_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
|
||||
_d.staticdir = HandlerTool(static.staticdir)
|
||||
_d.staticfile = HandlerTool(static.staticfile)
|
||||
_d.sessions = SessionTool()
|
||||
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
|
||||
_d.wsgiapp = WSGIAppTool(_wsgiapp.run)
|
||||
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
|
||||
_d.expires = Tool('before_finalize', _caching.expires)
|
||||
_d.tidy = Tool('before_finalize', tidy.tidy)
|
||||
_d.nsgmls = Tool('before_finalize', tidy.nsgmls)
|
||||
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
|
||||
_d.referer = Tool('before_request_body', cptools.referer)
|
||||
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
|
||||
_d.digest_auth = Tool('on_start_resource', auth.digest_auth)
|
||||
_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
|
||||
_d.flatten = Tool('before_finalize', cptools.flatten)
|
||||
_d.accept = Tool('on_start_resource', cptools.accept)
|
||||
_d.redirect = Tool('on_start_resource', cptools.redirect)
|
||||
|
||||
del _d, cptools, encoding, auth, static, tidy
|
||||
248
cherrypy/_cptree.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""CherryPy Application and Tree objects."""
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
|
||||
from cherrypy.lib import http as _http
|
||||
|
||||
|
||||
class Application(object):
|
||||
"""A CherryPy Application.
|
||||
|
||||
Servers and gateways should not instantiate Request objects directly.
|
||||
Instead, they should ask an Application object for a request object.
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object) for itself.
|
||||
"""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
root = None
|
||||
root__doc = """
|
||||
The top-most container of page handlers for this app. Handlers should
|
||||
be arranged in a hierarchy of attributes, matching the expected URI
|
||||
hierarchy; the default dispatcher then searches this hierarchy for a
|
||||
matching handler. When using a dispatcher other than the default,
|
||||
this value may be None."""
|
||||
|
||||
config = {}
|
||||
config__doc = """
|
||||
A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
|
||||
of {key: value} pairs."""
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet()
|
||||
toolboxes = {'tools': cherrypy.tools}
|
||||
|
||||
log = None
|
||||
log__doc = """A LogManager instance. See _cplogging."""
|
||||
|
||||
wsgiapp = None
|
||||
wsgiapp__doc = """A CPWSGIApp instance. See _cpwsgi."""
|
||||
|
||||
request_class = _cprequest.Request
|
||||
response_class = _cprequest.Response
|
||||
|
||||
relative_urls = False
|
||||
|
||||
def __init__(self, root, script_name="", config=None):
|
||||
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
|
||||
self.root = root
|
||||
self.script_name = script_name
|
||||
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
|
||||
|
||||
self.namespaces = self.namespaces.copy()
|
||||
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
|
||||
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
|
||||
|
||||
self.config = self.__class__.config.copy()
|
||||
if config:
|
||||
self.merge(config)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
|
||||
self.root, self.script_name)
|
||||
|
||||
script_name__doc = """
|
||||
The URI "mount point" for this app. A mount point is that portion of
|
||||
the URI which is constant for all URIs that are serviced by this
|
||||
application; it does not include scheme, host, or proxy ("virtual host")
|
||||
portions of the URI.
|
||||
|
||||
For example, if script_name is "/my/cool/app", then the URL
|
||||
"http://www.example.com/my/cool/app/page1" might be handled by a
|
||||
"page1" method on the root object.
|
||||
|
||||
The value of script_name MUST NOT end in a slash. If the script_name
|
||||
refers to the root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
If script_name is explicitly set to None, then the script_name will be
|
||||
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
|
||||
"""
|
||||
def _get_script_name(self):
|
||||
if self._script_name is None:
|
||||
# None signals that the script name should be pulled from WSGI environ.
|
||||
return cherrypy.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
|
||||
return self._script_name
|
||||
def _set_script_name(self, value):
|
||||
if value:
|
||||
value = value.rstrip("/")
|
||||
self._script_name = value
|
||||
script_name = property(fget=_get_script_name, fset=_set_script_name,
|
||||
doc=script_name__doc)
|
||||
|
||||
def merge(self, config):
|
||||
"""Merge the given config into self.config."""
|
||||
_cpconfig.merge(self.config, config)
|
||||
|
||||
# Handle namespaces specified in config.
|
||||
self.namespaces(self.config.get("/", {}))
|
||||
|
||||
def get_serving(self, local, remote, scheme, sproto):
|
||||
"""Create and return a Request and Response object."""
|
||||
req = self.request_class(local, remote, scheme, sproto)
|
||||
req.app = self
|
||||
|
||||
for name, toolbox in self.toolboxes.iteritems():
|
||||
req.namespaces[name] = toolbox
|
||||
|
||||
resp = self.response_class()
|
||||
cherrypy.serving.load(req, resp)
|
||||
cherrypy.engine.timeout_monitor.acquire()
|
||||
cherrypy.engine.publish('acquire_thread')
|
||||
|
||||
return req, resp
|
||||
|
||||
def release_serving(self):
|
||||
"""Release the current serving (request and response)."""
|
||||
req = cherrypy.serving.request
|
||||
|
||||
cherrypy.engine.timeout_monitor.release()
|
||||
|
||||
try:
|
||||
req.close()
|
||||
except:
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
|
||||
cherrypy.serving.clear()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgiapp(environ, start_response)
|
||||
|
||||
|
||||
class Tree(object):
|
||||
"""A registry of CherryPy applications, mounted at diverse points.
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object), in which case it dispatches to all
|
||||
mounted apps.
|
||||
"""
|
||||
|
||||
apps = {}
|
||||
apps__doc = """
|
||||
A dict of the form {script name: application}, where "script name"
|
||||
is a string declaring the URI mount point (no trailing slash), and
|
||||
"application" is an instance of cherrypy.Application (or an arbitrary
|
||||
WSGI callable if you happen to be using a WSGI server)."""
|
||||
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
def mount(self, root, script_name="", config=None):
|
||||
"""Mount a new app from a root object, script_name, and config.
|
||||
|
||||
root: an instance of a "controller class" (a collection of page
|
||||
handler methods) which represents the root of the application.
|
||||
This may also be an Application instance, or None if using
|
||||
a dispatcher other than the default.
|
||||
script_name: a string containing the "mount point" of the application.
|
||||
This should start with a slash, and be the path portion of the
|
||||
URL at which to mount the given root. For example, if root.index()
|
||||
will handle requests to "http://www.example.com:8080/dept/app1/",
|
||||
then the script_name argument would be "/dept/app1".
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the
|
||||
root of the URI, it MUST be an empty string (not "/").
|
||||
config: a file or dict containing application config.
|
||||
"""
|
||||
if script_name is None:
|
||||
raise TypeError(
|
||||
"The 'script_name' argument may not be None. Application "
|
||||
"objects may, however, possess a script_name of None (in "
|
||||
"order to inpect the WSGI environ for SCRIPT_NAME upon each "
|
||||
"request). You cannot mount such Applications on this Tree; "
|
||||
"you must pass them to a WSGI server interface directly.")
|
||||
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
|
||||
if isinstance(root, Application):
|
||||
app = root
|
||||
if script_name != "" and script_name != app.script_name:
|
||||
raise ValueError, "Cannot specify a different script name and pass an Application instance to cherrypy.mount"
|
||||
script_name = app.script_name
|
||||
else:
|
||||
app = Application(root, script_name)
|
||||
|
||||
# If mounted at "", add favicon.ico
|
||||
if (script_name == "" and root is not None
|
||||
and not hasattr(root, "favicon_ico")):
|
||||
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
|
||||
"favicon.ico")
|
||||
root.favicon_ico = tools.staticfile.handler(favicon)
|
||||
|
||||
if config:
|
||||
app.merge(config)
|
||||
|
||||
self.apps[script_name] = app
|
||||
|
||||
return app
|
||||
|
||||
def graft(self, wsgi_callable, script_name=""):
|
||||
"""Mount a wsgi callable at the given script_name."""
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
self.apps[script_name] = wsgi_callable
|
||||
|
||||
def script_name(self, path=None):
|
||||
"""The script_name of the app at the given path, or None.
|
||||
|
||||
If path is None, cherrypy.request is used.
|
||||
"""
|
||||
|
||||
if path is None:
|
||||
try:
|
||||
path = _http.urljoin(cherrypy.request.script_name,
|
||||
cherrypy.request.path_info)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
while True:
|
||||
if path in self.apps:
|
||||
return path
|
||||
|
||||
if path == "":
|
||||
return None
|
||||
|
||||
# Move one node up the tree and try again.
|
||||
path = path[:path.rfind("/")]
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# If you're calling this, then you're probably setting SCRIPT_NAME
|
||||
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
||||
# Try to look up the app using the full path.
|
||||
path = _http.urljoin(environ.get('SCRIPT_NAME', ''),
|
||||
environ.get('PATH_INFO', ''))
|
||||
sn = self.script_name(path or "/")
|
||||
if sn is None:
|
||||
start_response('404 Not Found', [])
|
||||
return []
|
||||
|
||||
app = self.apps[sn]
|
||||
|
||||
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
||||
environ = environ.copy()
|
||||
environ['SCRIPT_NAME'] = sn
|
||||
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
|
||||
return app(environ, start_response)
|
||||
|
||||
330
cherrypy/_cpwsgi.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""WSGI interface (see PEP 333)."""
|
||||
|
||||
import StringIO as _StringIO
|
||||
import sys as _sys
|
||||
|
||||
import cherrypy as _cherrypy
|
||||
from cherrypy import _cperror
|
||||
from cherrypy.lib import http as _http
|
||||
|
||||
|
||||
class VirtualHost(object):
|
||||
"""Select a different WSGI application based on the Host header.
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different applications. For example:
|
||||
|
||||
root = Root()
|
||||
RootApp = cherrypy.Application(root)
|
||||
Domain2App = cherrypy.Application(root)
|
||||
SecureApp = cherrypy.Application(Secure())
|
||||
|
||||
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
|
||||
domains={'www.domain2.example': Domain2App,
|
||||
'www.domain2.example:443': SecureApp,
|
||||
})
|
||||
|
||||
cherrypy.tree.graft(vhost)
|
||||
|
||||
default: required. The default WSGI application.
|
||||
|
||||
use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying.
|
||||
|
||||
domains: a dict of {host header value: application} pairs.
|
||||
The incoming "Host" request header is looked up in this dict,
|
||||
and, if a match is found, the corresponding WSGI application
|
||||
will be called instead of the default. Note that you often need
|
||||
separate entries for "example.com" and "www.example.com".
|
||||
In addition, "Host" headers may contain the port number.
|
||||
"""
|
||||
|
||||
def __init__(self, default, domains=None, use_x_forwarded_host=True):
|
||||
self.default = default
|
||||
self.domains = domains or {}
|
||||
self.use_x_forwarded_host = use_x_forwarded_host
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
domain = environ.get('HTTP_HOST', '')
|
||||
if self.use_x_forwarded_host:
|
||||
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
|
||||
|
||||
nextapp = self.domains.get(domain)
|
||||
if nextapp is None:
|
||||
nextapp = self.default
|
||||
return nextapp(environ, start_response)
|
||||
|
||||
|
||||
|
||||
# WSGI-to-CP Adapter #
|
||||
|
||||
|
||||
class AppResponse(object):
|
||||
|
||||
throws = (KeyboardInterrupt, SystemExit)
|
||||
request = None
|
||||
|
||||
def __init__(self, environ, start_response, cpapp, recursive=False):
|
||||
self.redirections = []
|
||||
self.recursive = recursive
|
||||
self.environ = environ
|
||||
self.start_response = start_response
|
||||
self.cpapp = cpapp
|
||||
self.setapp()
|
||||
|
||||
def setapp(self):
|
||||
try:
|
||||
self.request = self.get_request()
|
||||
s, h, b = self.get_response()
|
||||
self.iter_response = iter(b)
|
||||
self.write = self.start_response(s, h)
|
||||
except self.throws:
|
||||
self.close()
|
||||
raise
|
||||
except _cherrypy.InternalRedirect, ir:
|
||||
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
|
||||
self.close()
|
||||
self.iredirect(ir.path, ir.query_string)
|
||||
return
|
||||
except:
|
||||
if getattr(self.request, "throw_errors", False):
|
||||
self.close()
|
||||
raise
|
||||
|
||||
tb = _cperror.format_exc()
|
||||
_cherrypy.log(tb, severity=40)
|
||||
if not getattr(self.request, "show_tracebacks", True):
|
||||
tb = ""
|
||||
s, h, b = _cperror.bare_error(tb)
|
||||
self.iter_response = iter(b)
|
||||
|
||||
try:
|
||||
self.start_response(s, h, _sys.exc_info())
|
||||
except:
|
||||
# "The application must not trap any exceptions raised by
|
||||
# start_response, if it called start_response with exc_info.
|
||||
# Instead, it should allow such exceptions to propagate
|
||||
# back to the server or gateway."
|
||||
# But we still log and call close() to clean up ourselves.
|
||||
_cherrypy.log(traceback=True, severity=40)
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def iredirect(self, path, query_string):
|
||||
"""Doctor self.environ and perform an internal redirect.
|
||||
|
||||
When cherrypy.InternalRedirect is raised, this method is called.
|
||||
It rewrites the WSGI environ using the new path and query_string,
|
||||
and calls a new CherryPy Request object. Because the wsgi.input
|
||||
stream may have already been consumed by the next application,
|
||||
the redirected call will always be of HTTP method "GET"; therefore,
|
||||
any params must be passed in the query_string argument, which is
|
||||
formed from InternalRedirect.query_string when using that exception.
|
||||
If you need something more complicated, make and raise your own
|
||||
exception and write your own AppResponse subclass to trap it. ;)
|
||||
|
||||
It would be a bad idea to redirect after you've already yielded
|
||||
response content, although an enterprising soul could choose
|
||||
to abuse this.
|
||||
"""
|
||||
env = self.environ
|
||||
if not self.recursive:
|
||||
sn = env.get('SCRIPT_NAME', '')
|
||||
qs = query_string
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
if sn + path + qs in self.redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
"same URL twice: %r + %r + %r" %
|
||||
(sn, path, qs))
|
||||
else:
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
p = env.get('PATH_INFO', '')
|
||||
qs = env.get('QUERY_STRING', '')
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
self.redirections.append(sn + p + qs)
|
||||
|
||||
# Munge environment and try again.
|
||||
env['REQUEST_METHOD'] = "GET"
|
||||
env['PATH_INFO'] = path
|
||||
env['QUERY_STRING'] = query_string
|
||||
env['wsgi.input'] = _StringIO.StringIO()
|
||||
env['CONTENT_LENGTH'] = "0"
|
||||
|
||||
self.setapp()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
try:
|
||||
chunk = self.iter_response.next()
|
||||
# WSGI requires all data to be of type "str". This coercion should
|
||||
# not take any time at all if chunk is already of type "str".
|
||||
# If it's unicode, it could be a big performance hit (x ~500).
|
||||
if not isinstance(chunk, str):
|
||||
chunk = chunk.encode("ISO-8859-1")
|
||||
return chunk
|
||||
except self.throws:
|
||||
self.close()
|
||||
raise
|
||||
except _cherrypy.InternalRedirect, ir:
|
||||
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
|
||||
self.close()
|
||||
self.iredirect(ir.path, ir.query_string)
|
||||
except StopIteration:
|
||||
raise
|
||||
except:
|
||||
if getattr(self.request, "throw_errors", False):
|
||||
self.close()
|
||||
raise
|
||||
|
||||
tb = _cperror.format_exc()
|
||||
_cherrypy.log(tb, severity=40)
|
||||
if not getattr(self.request, "show_tracebacks", True):
|
||||
tb = ""
|
||||
s, h, b = _cperror.bare_error(tb)
|
||||
# Empty our iterable (so future calls raise StopIteration)
|
||||
self.iter_response = iter([])
|
||||
|
||||
try:
|
||||
self.start_response(s, h, _sys.exc_info())
|
||||
except:
|
||||
# "The application must not trap any exceptions raised by
|
||||
# start_response, if it called start_response with exc_info.
|
||||
# Instead, it should allow such exceptions to propagate
|
||||
# back to the server or gateway."
|
||||
# But we still log and call close() to clean up ourselves.
|
||||
_cherrypy.log(traceback=True, severity=40)
|
||||
self.close()
|
||||
raise
|
||||
|
||||
return "".join(b)
|
||||
|
||||
def close(self):
|
||||
"""Close and de-reference the current request and response. (Core)"""
|
||||
self.cpapp.release_serving()
|
||||
|
||||
def get_response(self):
|
||||
"""Run self.request and return its response."""
|
||||
meth = self.environ['REQUEST_METHOD']
|
||||
path = _http.urljoin(self.environ.get('SCRIPT_NAME', ''),
|
||||
self.environ.get('PATH_INFO', ''))
|
||||
qs = self.environ.get('QUERY_STRING', '')
|
||||
rproto = self.environ.get('SERVER_PROTOCOL')
|
||||
headers = self.translate_headers(self.environ)
|
||||
rfile = self.environ['wsgi.input']
|
||||
response = self.request.run(meth, path, qs, rproto, headers, rfile)
|
||||
return response.status, response.header_list, response.body
|
||||
|
||||
def get_request(self):
|
||||
"""Create a Request object using environ."""
|
||||
env = self.environ.get
|
||||
|
||||
local = _http.Host('', int(env('SERVER_PORT', 80)),
|
||||
env('SERVER_NAME', ''))
|
||||
remote = _http.Host(env('REMOTE_ADDR', ''),
|
||||
int(env('REMOTE_PORT', -1)),
|
||||
env('REMOTE_HOST', ''))
|
||||
scheme = env('wsgi.url_scheme')
|
||||
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
|
||||
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
|
||||
|
||||
# LOGON_USER is served by IIS, and is the name of the
|
||||
# user after having been mapped to a local account.
|
||||
# Both IIS and Apache set REMOTE_USER, when possible.
|
||||
request.login = env('LOGON_USER') or env('REMOTE_USER') or None
|
||||
request.multithread = self.environ['wsgi.multithread']
|
||||
request.multiprocess = self.environ['wsgi.multiprocess']
|
||||
request.wsgi_environ = self.environ
|
||||
request.prev = env('cherrypy.previous_request', None)
|
||||
return request
|
||||
|
||||
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
|
||||
'CONTENT_LENGTH': 'Content-Length',
|
||||
'CONTENT_TYPE': 'Content-Type',
|
||||
'REMOTE_HOST': 'Remote-Host',
|
||||
'REMOTE_ADDR': 'Remote-Addr',
|
||||
}
|
||||
|
||||
def translate_headers(self, environ):
|
||||
"""Translate CGI-environ header names to HTTP header names."""
|
||||
for cgiName in environ:
|
||||
# We assume all incoming header keys are uppercase already.
|
||||
if cgiName in self.headerNames:
|
||||
yield self.headerNames[cgiName], environ[cgiName]
|
||||
elif cgiName[:5] == "HTTP_":
|
||||
# Hackish attempt at recovering original header names.
|
||||
translatedHeader = cgiName[5:].replace("_", "-")
|
||||
yield translatedHeader, environ[cgiName]
|
||||
|
||||
|
||||
class CPWSGIApp(object):
|
||||
"""A WSGI application object for a CherryPy Application.
|
||||
|
||||
pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
|
||||
constructor that takes an initial, positional 'nextapp' argument,
|
||||
plus optional keyword arguments, and returns a WSGI application
|
||||
(that takes environ and start_response arguments). The 'name' can
|
||||
be any you choose, and will correspond to keys in self.config.
|
||||
|
||||
head: rather than nest all apps in the pipeline on each call, it's only
|
||||
done the first time, and the result is memoized into self.head. Set
|
||||
this to None again if you change self.pipeline after calling self.
|
||||
|
||||
config: a dict whose keys match names listed in the pipeline. Each
|
||||
value is a further dict which will be passed to the corresponding
|
||||
named WSGI callable (from the pipeline) as keyword arguments.
|
||||
"""
|
||||
|
||||
pipeline = []
|
||||
head = None
|
||||
config = {}
|
||||
|
||||
response_class = AppResponse
|
||||
|
||||
def __init__(self, cpapp, pipeline=None):
|
||||
self.cpapp = cpapp
|
||||
self.pipeline = self.pipeline[:]
|
||||
if pipeline:
|
||||
self.pipeline.extend(pipeline)
|
||||
self.config = self.config.copy()
|
||||
|
||||
def tail(self, environ, start_response):
|
||||
"""WSGI application callable for the actual CherryPy application.
|
||||
|
||||
You probably shouldn't call this; call self.__call__ instead,
|
||||
so that any WSGI middleware in self.pipeline can run first.
|
||||
"""
|
||||
return self.response_class(environ, start_response, self.cpapp)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
head = self.head
|
||||
if head is None:
|
||||
# Create and nest the WSGI apps in our pipeline (in reverse order).
|
||||
# Then memoize the result in self.head.
|
||||
head = self.tail
|
||||
for name, callable in self.pipeline[::-1]:
|
||||
conf = self.config.get(name, {})
|
||||
head = callable(head, **conf)
|
||||
self.head = head
|
||||
return head(environ, start_response)
|
||||
|
||||
def namespace_handler(self, k, v):
|
||||
"""Config handler for the 'wsgi' namespace."""
|
||||
if k == "pipeline":
|
||||
# Note this allows multiple 'wsgi.pipeline' config entries
|
||||
# (but each entry will be processed in a 'random' order).
|
||||
# It should also allow developers to set default middleware
|
||||
# in code (passed to self.__init__) that deployers can add to
|
||||
# (but not remove) via config.
|
||||
self.pipeline.extend(v)
|
||||
elif k == "response_class":
|
||||
self.response_class = v
|
||||
else:
|
||||
name, arg = k.split(".", 1)
|
||||
bucket = self.config.setdefault(name, {})
|
||||
bucket[arg] = v
|
||||
|
||||
68
cherrypy/_cpwsgi_server.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""WSGI server interface (see PEP 333). This adds some CP-specific bits to
|
||||
the framework-agnostic wsgiserver package.
|
||||
"""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import wsgiserver
|
||||
|
||||
|
||||
class CPHTTPRequest(wsgiserver.HTTPRequest):
|
||||
|
||||
def __init__(self, sendall, environ, wsgi_app):
|
||||
s = cherrypy.server
|
||||
self.max_request_header_size = s.max_request_header_size or 0
|
||||
self.max_request_body_size = s.max_request_body_size or 0
|
||||
wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app)
|
||||
|
||||
|
||||
class CPHTTPConnection(wsgiserver.HTTPConnection):
|
||||
|
||||
RequestHandlerClass = CPHTTPRequest
|
||||
|
||||
|
||||
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
|
||||
"""Wrapper for wsgiserver.CherryPyWSGIServer.
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
so that it can be used in other frameworks and applications. Therefore,
|
||||
we wrap it here, so we can set our own mount points from cherrypy.tree
|
||||
and apply some attributes from config -> cherrypy.server -> wsgiserver.
|
||||
"""
|
||||
|
||||
ConnectionClass = CPHTTPConnection
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
|
||||
# We have to make custom subclasses of wsgiserver internals here
|
||||
# so that our server.* attributes get applied to every request.
|
||||
class _CPHTTPRequest(wsgiserver.HTTPRequest):
|
||||
def __init__(self, sendall, environ, wsgi_app):
|
||||
s = server_adapter
|
||||
self.max_request_header_size = s.max_request_header_size or 0
|
||||
self.max_request_body_size = s.max_request_body_size or 0
|
||||
wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app)
|
||||
class _CPHTTPConnection(wsgiserver.HTTPConnection):
|
||||
RequestHandlerClass = _CPHTTPRequest
|
||||
self.ConnectionClass = _CPHTTPConnection
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
s = wsgiserver.CherryPyWSGIServer
|
||||
s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
|
||||
self.server_adapter.thread_pool,
|
||||
server_name,
|
||||
max = self.server_adapter.thread_pool_max,
|
||||
request_queue_size = self.server_adapter.socket_queue_size,
|
||||
timeout = self.server_adapter.socket_timeout,
|
||||
shutdown_timeout = self.server_adapter.shutdown_timeout,
|
||||
)
|
||||
self.protocol = self.server_adapter.protocol_version
|
||||
self.nodelay = self.server_adapter.nodelay
|
||||
self.ssl_context = self.server_adapter.ssl_context
|
||||
self.ssl_certificate = self.server_adapter.ssl_certificate
|
||||
self.ssl_certificate_chain = self.server_adapter.ssl_certificate_chain
|
||||
self.ssl_private_key = self.server_adapter.ssl_private_key
|
||||
|
||||
92
cherrypy/cherryd
Normal file
@@ -0,0 +1,92 @@
|
||||
#! /usr/bin/env python
|
||||
"""The CherryPy daemon."""
|
||||
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.process import plugins, servers
|
||||
|
||||
|
||||
def start(configfiles=None, daemonize=False, environment=None,
|
||||
fastcgi=False, scgi=False, pidfile=None, imports=None):
|
||||
"""Subscribe all engine plugins and start the engine."""
|
||||
sys.path = [''] + sys.path
|
||||
for i in imports or []:
|
||||
exec "import %s" % i
|
||||
|
||||
for c in configfiles or []:
|
||||
cherrypy.config.update(c)
|
||||
|
||||
engine = cherrypy.engine
|
||||
|
||||
if environment is not None:
|
||||
cherrypy.config.update({'environment': environment})
|
||||
|
||||
# Only daemonize if asked to.
|
||||
if daemonize:
|
||||
# Don't print anything to stdout/sterr.
|
||||
cherrypy.config.update({'log.screen': False})
|
||||
plugins.Daemonizer(engine).subscribe()
|
||||
|
||||
if pidfile:
|
||||
plugins.PIDFile(engine, pidfile).subscribe()
|
||||
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, "console_control_handler"):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
if fastcgi and scgi:
|
||||
# fastcgi and scgi aren't allowed together.
|
||||
cherrypy.log.error("fastcgi and scgi aren't allowed together.", 'ENGINE')
|
||||
sys.exit(1)
|
||||
elif fastcgi or scgi:
|
||||
# Turn off autoreload when using fastcgi or scgi.
|
||||
cherrypy.config.update({'engine.autoreload_on': False})
|
||||
# Turn off the default HTTP server (which is subscribed by default).
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
addr = cherrypy.server.bind_addr
|
||||
if fastcgi:
|
||||
f = servers.FlupFCGIServer(application=cherrypy.tree,
|
||||
bindAddress=addr)
|
||||
else:
|
||||
f = servers.FlupSCGIServer(application=cherrypy.tree,
|
||||
bindAddress=addr)
|
||||
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
|
||||
s.subscribe()
|
||||
|
||||
# Always start the engine; this will start all other services
|
||||
try:
|
||||
engine.start()
|
||||
except:
|
||||
# Assume the error has been logged already via bus.log.
|
||||
sys.exit(1)
|
||||
else:
|
||||
engine.block()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from optparse import OptionParser
|
||||
|
||||
p = OptionParser()
|
||||
p.add_option('-c', '--config', action="append", dest='config',
|
||||
help="specify config file(s)")
|
||||
p.add_option('-d', action="store_true", dest='daemonize',
|
||||
help="run the server as a daemon")
|
||||
p.add_option('-e', '--environment', dest='environment', default=None,
|
||||
help="apply the given config environment")
|
||||
p.add_option('-f', action="store_true", dest='fastcgi',
|
||||
help="start a fastcgi server instead of the default HTTP server")
|
||||
p.add_option('-s', action="store_true", dest='scgi',
|
||||
help="start a scgi server instead of the default HTTP server")
|
||||
p.add_option('-i', '--import', action="append", dest='imports',
|
||||
help="specify modules to import")
|
||||
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
|
||||
help="store the process id in the given file")
|
||||
options, args = p.parse_args()
|
||||
|
||||
start(options.config, options.daemonize,
|
||||
options.environment, options.fastcgi, options.scgi, options.pidfile,
|
||||
options.imports)
|
||||
|
||||
BIN
cherrypy/favicon.ico
Normal file
|
After Width: | Height: | Size: 52 KiB |
168
cherrypy/lib/__init__.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""CherryPy Library"""
|
||||
|
||||
import sys as _sys
|
||||
|
||||
|
||||
def modules(modulePath):
|
||||
"""Load a module and retrieve a reference to that module."""
|
||||
try:
|
||||
mod = _sys.modules[modulePath]
|
||||
if mod is None:
|
||||
raise KeyError()
|
||||
except KeyError:
|
||||
# The last [''] is important.
|
||||
mod = __import__(modulePath, globals(), locals(), [''])
|
||||
return mod
|
||||
|
||||
def attributes(full_attribute_name):
|
||||
"""Load a module and retrieve an attribute of that module."""
|
||||
|
||||
# Parse out the path, module, and attribute
|
||||
last_dot = full_attribute_name.rfind(u".")
|
||||
attr_name = full_attribute_name[last_dot + 1:]
|
||||
mod_path = full_attribute_name[:last_dot]
|
||||
|
||||
mod = modules(mod_path)
|
||||
# Let an AttributeError propagate outward.
|
||||
try:
|
||||
attr = getattr(mod, attr_name)
|
||||
except AttributeError:
|
||||
raise AttributeError("'%s' object has no attribute '%s'"
|
||||
% (mod_path, attr_name))
|
||||
|
||||
# Return a reference to the attribute.
|
||||
return attr
|
||||
|
||||
|
||||
# public domain "unrepr" implementation, found on the web and then improved.
|
||||
|
||||
class _Builder:
|
||||
|
||||
def build(self, o):
|
||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||
if m is None:
|
||||
raise TypeError("unrepr does not recognize %s" %
|
||||
repr(o.__class__.__name__))
|
||||
return m(o)
|
||||
|
||||
def build_Subscript(self, o):
|
||||
expr, flags, subs = o.getChildren()
|
||||
expr = self.build(expr)
|
||||
subs = self.build(subs)
|
||||
return expr[subs]
|
||||
|
||||
def build_CallFunc(self, o):
|
||||
children = map(self.build, o.getChildren())
|
||||
callee = children.pop(0)
|
||||
kwargs = children.pop() or {}
|
||||
starargs = children.pop() or ()
|
||||
args = tuple(children) + tuple(starargs)
|
||||
return callee(*args, **kwargs)
|
||||
|
||||
def build_List(self, o):
|
||||
return map(self.build, o.getChildren())
|
||||
|
||||
def build_Const(self, o):
|
||||
return o.value
|
||||
|
||||
def build_Dict(self, o):
|
||||
d = {}
|
||||
i = iter(map(self.build, o.getChildren()))
|
||||
for el in i:
|
||||
d[el] = i.next()
|
||||
return d
|
||||
|
||||
def build_Tuple(self, o):
|
||||
return tuple(self.build_List(o))
|
||||
|
||||
def build_Name(self, o):
|
||||
if o.name == 'None':
|
||||
return None
|
||||
if o.name == 'True':
|
||||
return True
|
||||
if o.name == 'False':
|
||||
return False
|
||||
|
||||
# See if the Name is a package or module. If it is, import it.
|
||||
try:
|
||||
return modules(o.name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# See if the Name is in __builtin__.
|
||||
try:
|
||||
import __builtin__
|
||||
return getattr(__builtin__, o.name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError("unrepr could not resolve the name %s" % repr(o.name))
|
||||
|
||||
def build_Add(self, o):
|
||||
left, right = map(self.build, o.getChildren())
|
||||
return left + right
|
||||
|
||||
def build_Getattr(self, o):
|
||||
parent = self.build(o.expr)
|
||||
return getattr(parent, o.attrname)
|
||||
|
||||
def build_NoneType(self, o):
|
||||
return None
|
||||
|
||||
def build_UnarySub(self, o):
|
||||
return -self.build(o.getChildren()[0])
|
||||
|
||||
def build_UnaryAdd(self, o):
|
||||
return self.build(o.getChildren()[0])
|
||||
|
||||
|
||||
def unrepr(s):
|
||||
"""Return a Python object compiled from a string."""
|
||||
if not s:
|
||||
return s
|
||||
|
||||
try:
|
||||
import compiler
|
||||
except ImportError:
|
||||
# Fallback to eval when compiler package is not available,
|
||||
# e.g. IronPython 1.0.
|
||||
return eval(s)
|
||||
|
||||
p = compiler.parse("__tempvalue__ = " + s)
|
||||
obj = p.getChildren()[1].getChildren()[0].getChildren()[1]
|
||||
|
||||
return _Builder().build(obj)
|
||||
|
||||
|
||||
class file_generator(object):
|
||||
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
|
||||
|
||||
def __init__(self, input, chunkSize=65536):
|
||||
self.input = input
|
||||
self.chunkSize = chunkSize
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
chunk = self.input.read(self.chunkSize)
|
||||
if chunk:
|
||||
return chunk
|
||||
else:
|
||||
self.input.close()
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
def file_generator_limited(fileobj, count, chunk_size=65536):
|
||||
"""Yield the given file object in chunks, stopping after `count`
|
||||
bytes has been emitted. Default chunk size is 64kB. (Core)
|
||||
"""
|
||||
remaining = count
|
||||
while remaining > 0:
|
||||
chunk = fileobj.read(min(chunk_size, remaining))
|
||||
chunklen = len(chunk)
|
||||
if chunklen == 0:
|
||||
return
|
||||
remaining -= chunklen
|
||||
yield chunk
|
||||
|
||||
75
cherrypy/lib/auth.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import cherrypy
|
||||
from cherrypy.lib import httpauth
|
||||
|
||||
|
||||
def check_auth(users, encrypt=None, realm=None):
|
||||
"""If an authorization header contains credentials, return True, else False."""
|
||||
if 'authorization' in cherrypy.request.headers:
|
||||
# make sure the provided credentials are correctly set
|
||||
ah = httpauth.parseAuthorization(cherrypy.request.headers['authorization'])
|
||||
if ah is None:
|
||||
raise cherrypy.HTTPError(400, 'Bad Request')
|
||||
|
||||
if not encrypt:
|
||||
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
|
||||
|
||||
if callable(users):
|
||||
try:
|
||||
# backward compatibility
|
||||
users = users() # expect it to return a dictionary
|
||||
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError, "Authentication users must be a dictionary"
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah["username"], None)
|
||||
except TypeError:
|
||||
# returns a password (encrypted or clear text)
|
||||
password = users(ah["username"])
|
||||
else:
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError, "Authentication users must be a dictionary"
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah["username"], None)
|
||||
|
||||
# validate the authorization by re-computing it here
|
||||
# and compare it with what the user-agent provided
|
||||
if httpauth.checkResponse(ah, password, method=cherrypy.request.method,
|
||||
encrypt=encrypt, realm=realm):
|
||||
cherrypy.request.login = ah["username"]
|
||||
return True
|
||||
|
||||
cherrypy.request.login = False
|
||||
return False
|
||||
|
||||
def basic_auth(realm, users, encrypt=None):
|
||||
"""If auth fails, raise 401 with a basic authentication header.
|
||||
|
||||
realm: a string containing the authentication realm.
|
||||
users: a dict of the form: {username: password} or a callable returning a dict.
|
||||
encrypt: callable used to encrypt the password returned from the user-agent.
|
||||
if None it defaults to a md5 encryption.
|
||||
"""
|
||||
if check_auth(users, encrypt):
|
||||
return
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
|
||||
|
||||
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
|
||||
|
||||
def digest_auth(realm, users):
|
||||
"""If auth fails, raise 401 with a digest authentication header.
|
||||
|
||||
realm: a string containing the authentication realm.
|
||||
users: a dict of the form: {username: password} or a callable returning a dict.
|
||||
"""
|
||||
if check_auth(users, realm=realm):
|
||||
return
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
|
||||
|
||||
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
|
||||
|
||||
245
cherrypy/lib/caching.py
Normal file
@@ -0,0 +1,245 @@
|
||||
import datetime
|
||||
import threading
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import cptools, http
|
||||
|
||||
|
||||
class MemoryCache:
|
||||
|
||||
maxobjects = 1000
|
||||
maxobj_size = 100000
|
||||
maxsize = 10000000
|
||||
delay = 600
|
||||
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
t = threading.Thread(target=self.expire_cache, name='expire_cache')
|
||||
self.expiration_thread = t
|
||||
if hasattr(threading.Thread, "daemon"):
|
||||
# Python 2.6+
|
||||
t.daemon = True
|
||||
else:
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
def clear(self):
|
||||
"""Reset the cache to its initial, empty state."""
|
||||
self.cache = {}
|
||||
self.expirations = {}
|
||||
self.tot_puts = 0
|
||||
self.tot_gets = 0
|
||||
self.tot_hist = 0
|
||||
self.tot_expires = 0
|
||||
self.tot_non_modified = 0
|
||||
self.cursize = 0
|
||||
|
||||
def key(self):
|
||||
return cherrypy.url(qs=cherrypy.request.query_string)
|
||||
|
||||
def expire_cache(self):
|
||||
# expire_cache runs in a separate thread which the servers are
|
||||
# not aware of. It's possible that "time" will be set to None
|
||||
# arbitrarily, so we check "while time" to avoid exceptions.
|
||||
# See tickets #99 and #180 for more information.
|
||||
while time:
|
||||
now = time.time()
|
||||
for expiration_time, objects in self.expirations.items():
|
||||
if expiration_time <= now:
|
||||
for obj_size, obj_key in objects:
|
||||
try:
|
||||
del self.cache[obj_key]
|
||||
self.tot_expires += 1
|
||||
self.cursize -= obj_size
|
||||
except KeyError:
|
||||
# the key may have been deleted elsewhere
|
||||
pass
|
||||
del self.expirations[expiration_time]
|
||||
time.sleep(0.1)
|
||||
|
||||
def get(self):
|
||||
"""Return the object if in the cache, else None."""
|
||||
self.tot_gets += 1
|
||||
cache_item = self.cache.get(self.key(), None)
|
||||
if cache_item:
|
||||
self.tot_hist += 1
|
||||
return cache_item
|
||||
else:
|
||||
return None
|
||||
|
||||
def put(self, obj):
|
||||
if len(self.cache) < self.maxobjects:
|
||||
# Size check no longer includes header length
|
||||
obj_size = len(obj[2])
|
||||
total_size = self.cursize + obj_size
|
||||
|
||||
# checks if there's space for the object
|
||||
if (obj_size < self.maxobj_size and total_size < self.maxsize):
|
||||
# add to the expirations list and cache
|
||||
expiration_time = cherrypy.response.time + self.delay
|
||||
obj_key = self.key()
|
||||
bucket = self.expirations.setdefault(expiration_time, [])
|
||||
bucket.append((obj_size, obj_key))
|
||||
self.cache[obj_key] = obj
|
||||
self.tot_puts += 1
|
||||
self.cursize = total_size
|
||||
|
||||
def delete(self):
|
||||
self.cache.pop(self.key(), None)
|
||||
|
||||
|
||||
def get(invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
|
||||
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
|
||||
|
||||
If POST, PUT, or DELETE:
|
||||
* invalidates (deletes) any cached response for this resource
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = False
|
||||
|
||||
else if a cached copy exists:
|
||||
* sets request.cached = True
|
||||
* sets request.cacheable = False
|
||||
* sets response.headers to the cached values
|
||||
* checks the cached Last-Modified response header against the
|
||||
current If-(Un)Modified-Since request headers; raises 304
|
||||
if necessary.
|
||||
* sets response.status and response.body to the cached values
|
||||
* returns True
|
||||
|
||||
otherwise:
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = True
|
||||
* returns False
|
||||
"""
|
||||
request = cherrypy.request
|
||||
|
||||
# POST, PUT, DELETE should invalidate (delete) the cached copy.
|
||||
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
|
||||
if request.method in invalid_methods:
|
||||
cherrypy._cache.delete()
|
||||
request.cached = False
|
||||
request.cacheable = False
|
||||
return False
|
||||
|
||||
cache_data = cherrypy._cache.get()
|
||||
request.cached = c = bool(cache_data)
|
||||
request.cacheable = not c
|
||||
if c:
|
||||
response = cherrypy.response
|
||||
s, h, b, create_time, original_req_headers = cache_data
|
||||
|
||||
# Check 'Vary' selecting headers. If any headers mentioned in "Vary"
|
||||
# differ between the cached and current request, bail out and
|
||||
# let the rest of CP handle the request. This should properly
|
||||
# mimic the behavior of isolated caches as RFC 2616 assumes:
|
||||
# "If the selecting request header fields for the cached entry
|
||||
# do not match the selecting request header fields of the new
|
||||
# request, then the cache MUST NOT use a cached entry to satisfy
|
||||
# the request unless it first relays the new request to the origin
|
||||
# server in a conditional request and the server responds with
|
||||
# 304 (Not Modified), including an entity tag or Content-Location
|
||||
# that indicates the entity to be used.
|
||||
# TODO: can we store multiple variants based on Vary'd headers?
|
||||
for header_element in h.elements('Vary'):
|
||||
key = header_element.value
|
||||
if original_req_headers[key] != request.headers.get(key, 'missing'):
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
# Copy the response headers. See http://www.cherrypy.org/ticket/721.
|
||||
response.headers = rh = http.HeaderMap()
|
||||
for k in h:
|
||||
dict.__setitem__(rh, k, dict.__getitem__(h, k))
|
||||
|
||||
# Add the required Age header
|
||||
response.headers["Age"] = str(int(response.time - create_time))
|
||||
|
||||
try:
|
||||
# Note that validate_since depends on a Last-Modified header;
|
||||
# this was put into the cached copy, and should have been
|
||||
# resurrected just above (response.headers = cache_data[1]).
|
||||
cptools.validate_since()
|
||||
except cherrypy.HTTPRedirect, x:
|
||||
if x.status == 304:
|
||||
cherrypy._cache.tot_non_modified += 1
|
||||
raise
|
||||
|
||||
# serve it & get out from the request
|
||||
response.status = s
|
||||
response.body = b
|
||||
return c
|
||||
|
||||
|
||||
def tee_output():
|
||||
def tee(body):
|
||||
"""Tee response.body into a list."""
|
||||
output = []
|
||||
for chunk in body:
|
||||
output.append(chunk)
|
||||
yield chunk
|
||||
|
||||
# Might as well do this here; why cache if the body isn't consumed?
|
||||
if response.headers.get('Pragma', None) != 'no-cache':
|
||||
# save the cache data
|
||||
body = ''.join(output)
|
||||
vary = [he.value for he in
|
||||
cherrypy.response.headers.elements('Vary')]
|
||||
if vary:
|
||||
sel_headers = dict([(k, v) for k, v
|
||||
in cherrypy.request.headers.iteritems()
|
||||
if k in vary])
|
||||
else:
|
||||
sel_headers = {}
|
||||
cherrypy._cache.put((response.status, response.headers or {},
|
||||
body, response.time, sel_headers))
|
||||
|
||||
response = cherrypy.response
|
||||
response.body = tee(response.body)
|
||||
|
||||
|
||||
def expires(secs=0, force=False):
|
||||
"""Tool for influencing cache mechanisms using the 'Expires' header.
|
||||
|
||||
'secs' must be either an int or a datetime.timedelta, and indicates the
|
||||
number of seconds between response.time and when the response should
|
||||
expire. The 'Expires' header will be set to (response.time + secs).
|
||||
|
||||
If 'secs' is zero, the 'Expires' header is set one year in the past, and
|
||||
the following "cache prevention" headers are also set:
|
||||
'Pragma': 'no-cache'
|
||||
'Cache-Control': 'no-cache, must-revalidate'
|
||||
|
||||
If 'force' is False (the default), the following headers are checked:
|
||||
'Etag', 'Last-Modified', 'Age', 'Expires'. If any are already present,
|
||||
none of the above response headers are set.
|
||||
"""
|
||||
|
||||
response = cherrypy.response
|
||||
headers = response.headers
|
||||
|
||||
cacheable = False
|
||||
if not force:
|
||||
# some header names that indicate that the response can be cached
|
||||
for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
|
||||
if indicator in headers:
|
||||
cacheable = True
|
||||
break
|
||||
|
||||
if not cacheable:
|
||||
if isinstance(secs, datetime.timedelta):
|
||||
secs = (86400 * secs.days) + secs.seconds
|
||||
|
||||
if secs == 0:
|
||||
if force or "Pragma" not in headers:
|
||||
headers["Pragma"] = "no-cache"
|
||||
if cherrypy.request.protocol >= (1, 1):
|
||||
if force or "Cache-Control" not in headers:
|
||||
headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
# Set an explicit Expires date in the past.
|
||||
expiry = http.HTTPDate(1169942400.0)
|
||||
else:
|
||||
expiry = http.HTTPDate(response.time + secs)
|
||||
if force or "Expires" not in headers:
|
||||
headers["Expires"] = expiry
|
||||
361
cherrypy/lib/covercp.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""Code-coverage tools for CherryPy.
|
||||
|
||||
To use this module, or the coverage tools in the test suite,
|
||||
you need to download 'coverage.py', either Gareth Rees' original
|
||||
implementation:
|
||||
http://www.garethrees.org/2001/12/04/python-coverage/
|
||||
|
||||
or Ned Batchelder's enhanced version:
|
||||
http://www.nedbatchelder.com/code/modules/coverage.html
|
||||
|
||||
To turn on coverage tracing, use the following code:
|
||||
|
||||
cherrypy.engine.subscribe('start', covercp.start)
|
||||
cherrypy.engine.subscribe('start_thread', covercp.start)
|
||||
|
||||
Run your code, then use the covercp.serve() function to browse the
|
||||
results in a web browser. If you run this module from the command line,
|
||||
it will call serve() for you.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import cgi
|
||||
import urllib
|
||||
import os, os.path
|
||||
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
|
||||
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
try:
|
||||
from coverage import the_coverage as coverage
|
||||
def start(threadid=None):
|
||||
coverage.start()
|
||||
except ImportError:
|
||||
# Setting coverage to None will raise errors
|
||||
# that need to be trapped downstream.
|
||||
coverage = None
|
||||
|
||||
import warnings
|
||||
warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
|
||||
|
||||
def start(threadid=None):
|
||||
pass
|
||||
start.priority = 20
|
||||
|
||||
# Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff
|
||||
import cherrypy
|
||||
initial_base = os.path.dirname(cherrypy.__file__)
|
||||
|
||||
TEMPLATE_MENU = """<html>
|
||||
<head>
|
||||
<title>CherryPy Coverage Menu</title>
|
||||
<style>
|
||||
body {font: 9pt Arial, serif;}
|
||||
#tree {
|
||||
font-size: 8pt;
|
||||
font-family: Andale Mono, monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
#tree a:active, a:focus {
|
||||
background-color: black;
|
||||
padding: 1px;
|
||||
color: white;
|
||||
border: 0px solid #9999FF;
|
||||
-moz-outline-style: none;
|
||||
}
|
||||
.fail { color: red;}
|
||||
.pass { color: #888;}
|
||||
#pct { text-align: right;}
|
||||
h3 {
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
}
|
||||
input { border: 1px solid #ccc; padding: 2px; }
|
||||
.directory {
|
||||
color: #933;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.file {
|
||||
color: #400;
|
||||
}
|
||||
a { text-decoration: none; }
|
||||
#crumbs {
|
||||
color: white;
|
||||
font-size: 8pt;
|
||||
font-family: Andale Mono, monospace;
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
#crumbs a {
|
||||
color: #f88;
|
||||
}
|
||||
#options {
|
||||
line-height: 2.3em;
|
||||
border: 1px solid black;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
}
|
||||
#exclude {
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
#submit {
|
||||
background-color: black;
|
||||
color: white;
|
||||
border: 0;
|
||||
margin-bottom: -9px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>CherryPy Coverage</h2>"""
|
||||
|
||||
TEMPLATE_FORM = """
|
||||
<div id="options">
|
||||
<form action='menu' method=GET>
|
||||
<input type='hidden' name='base' value='%(base)s' />
|
||||
Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
|
||||
Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
|
||||
Exclude files matching<br />
|
||||
<input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
|
||||
<br />
|
||||
|
||||
<input type='submit' value='Change view' id="submit"/>
|
||||
</form>
|
||||
</div>"""
|
||||
|
||||
TEMPLATE_FRAMESET = """<html>
|
||||
<head><title>CherryPy coverage data</title></head>
|
||||
<frameset cols='250, 1*'>
|
||||
<frame src='menu?base=%s' />
|
||||
<frame name='main' src='' />
|
||||
</frameset>
|
||||
</html>
|
||||
""" % initial_base.lower()
|
||||
|
||||
TEMPLATE_COVERAGE = """<html>
|
||||
<head>
|
||||
<title>Coverage for %(name)s</title>
|
||||
<style>
|
||||
h2 { margin-bottom: .25em; }
|
||||
p { margin: .25em; }
|
||||
.covered { color: #000; background-color: #fff; }
|
||||
.notcovered { color: #fee; background-color: #500; }
|
||||
.excluded { color: #00f; background-color: #fff; }
|
||||
table .covered, table .notcovered, table .excluded
|
||||
{ font-family: Andale Mono, monospace;
|
||||
font-size: 10pt; white-space: pre; }
|
||||
|
||||
.lineno { background-color: #eee;}
|
||||
.notcovered .lineno { background-color: #000;}
|
||||
table { border-collapse: collapse;
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%(name)s</h2>
|
||||
<p>%(fullpath)s</p>
|
||||
<p>Coverage: %(pc)s%%</p>"""
|
||||
|
||||
TEMPLATE_LOC_COVERED = """<tr class="covered">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
|
||||
TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
|
||||
|
||||
def _percent(statements, missing):
|
||||
s = len(statements)
|
||||
e = s - len(missing)
|
||||
if s > 0:
|
||||
return int(round(100.0 * e / s))
|
||||
return 0
|
||||
|
||||
def _show_branch(root, base, path, pct=0, showpct=False, exclude=""):
|
||||
|
||||
# Show the directory name and any of our children
|
||||
dirs = [k for k, v in root.iteritems() if v]
|
||||
dirs.sort()
|
||||
for name in dirs:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
if newpath.lower().startswith(base):
|
||||
relpath = newpath[len(base):]
|
||||
yield "| " * relpath.count(os.sep)
|
||||
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
|
||||
(newpath, urllib.quote_plus(exclude), name)
|
||||
|
||||
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude):
|
||||
yield chunk
|
||||
|
||||
# Now list the files
|
||||
if path.lower().startswith(base):
|
||||
relpath = path[len(base):]
|
||||
files = [k for k, v in root.iteritems() if not v]
|
||||
files.sort()
|
||||
for name in files:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
pc_str = ""
|
||||
if showpct:
|
||||
try:
|
||||
_, statements, _, missing, _ = coverage.analysis2(newpath)
|
||||
except:
|
||||
# Yes, we really want to pass on all errors.
|
||||
pass
|
||||
else:
|
||||
pc = _percent(statements, missing)
|
||||
pc_str = ("%3d%% " % pc).replace(' ',' ')
|
||||
if pc < float(pct) or pc == -1:
|
||||
pc_str = "<span class='fail'>%s</span>" % pc_str
|
||||
else:
|
||||
pc_str = "<span class='pass'>%s</span>" % pc_str
|
||||
|
||||
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
|
||||
pc_str, newpath, name)
|
||||
|
||||
def _skip_file(path, exclude):
|
||||
if exclude:
|
||||
return bool(re.search(exclude, path))
|
||||
|
||||
def _graft(path, tree):
|
||||
d = tree
|
||||
|
||||
p = path
|
||||
atoms = []
|
||||
while True:
|
||||
p, tail = os.path.split(p)
|
||||
if not tail:
|
||||
break
|
||||
atoms.append(tail)
|
||||
atoms.append(p)
|
||||
if p != "/":
|
||||
atoms.append("/")
|
||||
|
||||
atoms.reverse()
|
||||
for node in atoms:
|
||||
if node:
|
||||
d = d.setdefault(node, {})
|
||||
|
||||
def get_tree(base, exclude):
|
||||
"""Return covered module names as a nested dict."""
|
||||
tree = {}
|
||||
coverage.get_ready()
|
||||
runs = coverage.cexecuted.keys()
|
||||
if runs:
|
||||
for path in runs:
|
||||
if not _skip_file(path, exclude) and not os.path.isdir(path):
|
||||
_graft(path, tree)
|
||||
return tree
|
||||
|
||||
class CoverStats(object):
|
||||
|
||||
def index(self):
|
||||
return TEMPLATE_FRAMESET
|
||||
index.exposed = True
|
||||
|
||||
def menu(self, base="/", pct="50", showpct="",
|
||||
exclude=r'python\d\.\d|test|tut\d|tutorial'):
|
||||
|
||||
# The coverage module uses all-lower-case names.
|
||||
base = base.lower().rstrip(os.sep)
|
||||
|
||||
yield TEMPLATE_MENU
|
||||
yield TEMPLATE_FORM % locals()
|
||||
|
||||
# Start by showing links for parent paths
|
||||
yield "<div id='crumbs'>"
|
||||
path = ""
|
||||
atoms = base.split(os.sep)
|
||||
atoms.pop()
|
||||
for atom in atoms:
|
||||
path += atom + os.sep
|
||||
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
|
||||
% (path, urllib.quote_plus(exclude), atom, os.sep))
|
||||
yield "</div>"
|
||||
|
||||
yield "<div id='tree'>"
|
||||
|
||||
# Then display the tree
|
||||
tree = get_tree(base, exclude)
|
||||
if not tree:
|
||||
yield "<p>No modules covered.</p>"
|
||||
else:
|
||||
for chunk in _show_branch(tree, base, "/", pct,
|
||||
showpct=='checked', exclude):
|
||||
yield chunk
|
||||
|
||||
yield "</div>"
|
||||
yield "</body></html>"
|
||||
menu.exposed = True
|
||||
|
||||
def annotated_file(self, filename, statements, excluded, missing):
|
||||
source = open(filename, 'r')
|
||||
buffer = []
|
||||
for lineno, line in enumerate(source.readlines()):
|
||||
lineno += 1
|
||||
line = line.strip("\n\r")
|
||||
empty_the_buffer = True
|
||||
if lineno in excluded:
|
||||
template = TEMPLATE_LOC_EXCLUDED
|
||||
elif lineno in missing:
|
||||
template = TEMPLATE_LOC_NOT_COVERED
|
||||
elif lineno in statements:
|
||||
template = TEMPLATE_LOC_COVERED
|
||||
else:
|
||||
empty_the_buffer = False
|
||||
buffer.append((lineno, line))
|
||||
if empty_the_buffer:
|
||||
for lno, pastline in buffer:
|
||||
yield template % (lno, cgi.escape(pastline))
|
||||
buffer = []
|
||||
yield template % (lineno, cgi.escape(line))
|
||||
|
||||
def report(self, name):
|
||||
coverage.get_ready()
|
||||
filename, statements, excluded, missing, _ = coverage.analysis2(name)
|
||||
pc = _percent(statements, missing)
|
||||
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
|
||||
fullpath=name,
|
||||
pc=pc)
|
||||
yield '<table>\n'
|
||||
for line in self.annotated_file(filename, statements, excluded,
|
||||
missing):
|
||||
yield line
|
||||
yield '</table>'
|
||||
yield '</body>'
|
||||
yield '</html>'
|
||||
report.exposed = True
|
||||
|
||||
|
||||
def serve(path=localFile, port=8080):
|
||||
if coverage is None:
|
||||
raise ImportError("The coverage module could not be imported.")
|
||||
coverage.cache_default = path
|
||||
|
||||
import cherrypy
|
||||
cherrypy.config.update({'server.socket_port': port,
|
||||
'server.thread_pool': 10,
|
||||
'environment': "production",
|
||||
})
|
||||
cherrypy.quickstart(CoverStats())
|
||||
|
||||
if __name__ == "__main__":
|
||||
serve(*tuple(sys.argv[1:]))
|
||||
|
||||
433
cherrypy/lib/cptools.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""Functions for builtin CherryPy tools."""
|
||||
|
||||
import logging
|
||||
try:
|
||||
# Python 2.5+
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import new as md5
|
||||
import re
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import http as _http
|
||||
|
||||
|
||||
# Conditional HTTP request support #
|
||||
|
||||
def validate_etags(autotags=False):
|
||||
"""Validate the current ETag against If-Match, If-None-Match headers.
|
||||
|
||||
If autotags is True, an ETag response-header value will be provided
|
||||
from an MD5 hash of the response body (unless some other code has
|
||||
already provided an ETag header). If False (the default), the ETag
|
||||
will not be automatic.
|
||||
|
||||
WARNING: the autotags feature is not designed for URL's which allow
|
||||
methods other than GET. For example, if a POST to the same URL returns
|
||||
no content, the automatic ETag will be incorrect, breaking a fundamental
|
||||
use for entity tags in a possibly destructive fashion. Likewise, if you
|
||||
raise 304 Not Modified, the response body will be empty, the ETag hash
|
||||
will be incorrect, and your application will break.
|
||||
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
|
||||
"""
|
||||
response = cherrypy.response
|
||||
|
||||
# Guard against being run twice.
|
||||
if hasattr(response, "ETag"):
|
||||
return
|
||||
|
||||
status, reason, msg = _http.valid_status(response.status)
|
||||
|
||||
etag = response.headers.get('ETag')
|
||||
|
||||
# Automatic ETag generation. See warning in docstring.
|
||||
if (not etag) and autotags:
|
||||
if status == 200:
|
||||
etag = response.collapse_body()
|
||||
etag = '"%s"' % md5(etag).hexdigest()
|
||||
response.headers['ETag'] = etag
|
||||
|
||||
response.ETag = etag
|
||||
|
||||
# "If the request would, without the If-Match header field, result in
|
||||
# anything other than a 2xx or 412 status, then the If-Match header
|
||||
# MUST be ignored."
|
||||
if status >= 200 and status <= 299:
|
||||
request = cherrypy.request
|
||||
|
||||
conditions = request.headers.elements('If-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if conditions and not (conditions == ["*"] or etag in conditions):
|
||||
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
|
||||
"not match %r" % (etag, conditions))
|
||||
|
||||
conditions = request.headers.elements('If-None-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if conditions == ["*"] or etag in conditions:
|
||||
if request.method in ("GET", "HEAD"):
|
||||
raise cherrypy.HTTPRedirect([], 304)
|
||||
else:
|
||||
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
|
||||
"matched %r" % (etag, conditions))
|
||||
|
||||
def validate_since():
|
||||
"""Validate the current Last-Modified against If-Modified-Since headers.
|
||||
|
||||
If no code has set the Last-Modified response header, then no validation
|
||||
will be performed.
|
||||
"""
|
||||
response = cherrypy.response
|
||||
lastmod = response.headers.get('Last-Modified')
|
||||
if lastmod:
|
||||
status, reason, msg = _http.valid_status(response.status)
|
||||
|
||||
request = cherrypy.request
|
||||
|
||||
since = request.headers.get('If-Unmodified-Since')
|
||||
if since and since != lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 412:
|
||||
raise cherrypy.HTTPError(412)
|
||||
|
||||
since = request.headers.get('If-Modified-Since')
|
||||
if since and since == lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 304:
|
||||
if request.method in ("GET", "HEAD"):
|
||||
raise cherrypy.HTTPRedirect([], 304)
|
||||
else:
|
||||
raise cherrypy.HTTPError(412)
|
||||
|
||||
|
||||
# Tool code #
|
||||
|
||||
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
scheme='X-Forwarded-Proto'):
|
||||
"""Change the base URL (scheme://host[:port][/path]).
|
||||
|
||||
For running a CP server behind Apache, lighttpd, or other HTTP server.
|
||||
|
||||
If you want the new request.base to include path info (not just the host),
|
||||
you must explicitly set base to the full base path, and ALSO set 'local'
|
||||
to '', so that the X-Forwarded-Host request header (which never includes
|
||||
path info) does not override it. Regardless, the value for 'base' MUST
|
||||
NOT end in a slash.
|
||||
|
||||
cherrypy.request.remote.ip (the IP address of the client) will be
|
||||
rewritten if the header specified by the 'remote' arg is valid.
|
||||
By default, 'remote' is set to 'X-Forwarded-For'. If you do not
|
||||
want to rewrite remote.ip, set the 'remote' arg to an empty string.
|
||||
"""
|
||||
|
||||
request = cherrypy.request
|
||||
|
||||
if scheme:
|
||||
s = request.headers.get(scheme, None)
|
||||
if s == 'on' and 'ssl' in scheme.lower():
|
||||
# This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
|
||||
scheme = 'https'
|
||||
else:
|
||||
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
|
||||
scheme = s
|
||||
if not scheme:
|
||||
scheme = request.base[:request.base.find("://")]
|
||||
|
||||
if local:
|
||||
base = request.headers.get(local, base)
|
||||
if not base:
|
||||
port = cherrypy.request.local.port
|
||||
if port == 80:
|
||||
base = '127.0.0.1'
|
||||
else:
|
||||
base = '127.0.0.1:%s' % port
|
||||
|
||||
if base.find("://") == -1:
|
||||
# add http:// or https:// if needed
|
||||
base = scheme + "://" + base
|
||||
|
||||
request.base = base
|
||||
|
||||
if remote:
|
||||
xff = request.headers.get(remote)
|
||||
if xff:
|
||||
if remote == 'X-Forwarded-For':
|
||||
# See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
|
||||
xff = xff.split(',')[-1].strip()
|
||||
request.remote.ip = xff
|
||||
|
||||
|
||||
def ignore_headers(headers=('Range',)):
|
||||
"""Delete request headers whose field names are included in 'headers'.
|
||||
|
||||
This is a useful tool for working behind certain HTTP servers;
|
||||
for example, Apache duplicates the work that CP does for 'Range'
|
||||
headers, and will doubly-truncate the response.
|
||||
"""
|
||||
request = cherrypy.request
|
||||
for name in headers:
|
||||
if name in request.headers:
|
||||
del request.headers[name]
|
||||
|
||||
|
||||
def response_headers(headers=None):
|
||||
"""Set headers on the response."""
|
||||
for name, value in (headers or []):
|
||||
cherrypy.response.headers[name] = value
|
||||
response_headers.failsafe = True
|
||||
|
||||
|
||||
def referer(pattern, accept=True, accept_missing=False, error=403,
|
||||
message='Forbidden Referer header.'):
|
||||
"""Raise HTTPError if Referer header does/does not match the given pattern.
|
||||
|
||||
pattern: a regular expression pattern to test against the Referer.
|
||||
accept: if True, the Referer must match the pattern; if False,
|
||||
the Referer must NOT match the pattern.
|
||||
accept_missing: if True, permit requests with no Referer header.
|
||||
error: the HTTP error code to return to the client on failure.
|
||||
message: a string to include in the response body on failure.
|
||||
"""
|
||||
try:
|
||||
match = bool(re.match(pattern, cherrypy.request.headers['Referer']))
|
||||
if accept == match:
|
||||
return
|
||||
except KeyError:
|
||||
if accept_missing:
|
||||
return
|
||||
|
||||
raise cherrypy.HTTPError(error, message)
|
||||
|
||||
|
||||
class SessionAuth(object):
|
||||
"""Assert that the user is logged in."""
|
||||
|
||||
session_key = "username"
|
||||
|
||||
def check_username_and_password(self, username, password):
|
||||
pass
|
||||
|
||||
def anonymous(self):
|
||||
"""Provide a temporary user name for anonymous users."""
|
||||
pass
|
||||
|
||||
def on_login(self, username):
|
||||
pass
|
||||
|
||||
def on_logout(self, username):
|
||||
pass
|
||||
|
||||
def on_check(self, username):
|
||||
pass
|
||||
|
||||
def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
|
||||
return """<html><body>
|
||||
Message: %(error_msg)s
|
||||
<form method="post" action="do_login">
|
||||
Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
|
||||
Password: <input type="password" name="password" size="10" /><br />
|
||||
<input type="hidden" name="from_page" value="%(from_page)s" /><br />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</body></html>""" % {'from_page': from_page, 'username': username,
|
||||
'error_msg': error_msg}
|
||||
|
||||
def do_login(self, username, password, from_page='..', **kwargs):
|
||||
"""Login. May raise redirect, or return True if request handled."""
|
||||
error_msg = self.check_username_and_password(username, password)
|
||||
if error_msg:
|
||||
body = self.login_screen(from_page, username, error_msg)
|
||||
cherrypy.response.body = body
|
||||
if cherrypy.response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del cherrypy.response.headers["Content-Length"]
|
||||
return True
|
||||
else:
|
||||
cherrypy.session[self.session_key] = cherrypy.request.login = username
|
||||
self.on_login(username)
|
||||
raise cherrypy.HTTPRedirect(from_page or "/")
|
||||
|
||||
def do_logout(self, from_page='..', **kwargs):
|
||||
"""Logout. May raise redirect, or return True if request handled."""
|
||||
sess = cherrypy.session
|
||||
username = sess.get(self.session_key)
|
||||
sess[self.session_key] = None
|
||||
if username:
|
||||
cherrypy.request.login = None
|
||||
self.on_logout(username)
|
||||
raise cherrypy.HTTPRedirect(from_page)
|
||||
|
||||
def do_check(self):
|
||||
"""Assert username. May raise redirect, or return True if request handled."""
|
||||
sess = cherrypy.session
|
||||
request = cherrypy.request
|
||||
|
||||
username = sess.get(self.session_key)
|
||||
if not username:
|
||||
sess[self.session_key] = username = self.anonymous()
|
||||
if not username:
|
||||
cherrypy.response.body = self.login_screen(cherrypy.url(qs=request.query_string))
|
||||
if cherrypy.response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del cherrypy.response.headers["Content-Length"]
|
||||
return True
|
||||
cherrypy.request.login = username
|
||||
self.on_check(username)
|
||||
|
||||
def run(self):
|
||||
request = cherrypy.request
|
||||
path = request.path_info
|
||||
if path.endswith('login_screen'):
|
||||
return self.login_screen(**request.params)
|
||||
elif path.endswith('do_login'):
|
||||
return self.do_login(**request.params)
|
||||
elif path.endswith('do_logout'):
|
||||
return self.do_logout(**request.params)
|
||||
else:
|
||||
return self.do_check()
|
||||
|
||||
|
||||
def session_auth(**kwargs):
|
||||
sa = SessionAuth()
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(sa, k, v)
|
||||
return sa.run()
|
||||
session_auth.__doc__ = """Session authentication hook.
|
||||
|
||||
Any attribute of the SessionAuth class may be overridden via a keyword arg
|
||||
to this function:
|
||||
|
||||
""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
|
||||
for k in dir(SessionAuth) if not k.startswith("__")])
|
||||
|
||||
|
||||
def log_traceback(severity=logging.DEBUG):
|
||||
"""Write the last error's traceback to the cherrypy error log."""
|
||||
cherrypy.log("", "HTTP", severity=severity, traceback=True)
|
||||
|
||||
def log_request_headers():
|
||||
"""Write request headers to the cherrypy error log."""
|
||||
h = [" %s: %s" % (k, v) for k, v in cherrypy.request.header_list]
|
||||
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
|
||||
|
||||
def log_hooks():
|
||||
"""Write request.hooks to the cherrypy error log."""
|
||||
msg = []
|
||||
# Sort by the standard points if possible.
|
||||
from cherrypy import _cprequest
|
||||
points = _cprequest.hookpoints
|
||||
for k in cherrypy.request.hooks.keys():
|
||||
if k not in points:
|
||||
points.append(k)
|
||||
|
||||
for k in points:
|
||||
msg.append(" %s:" % k)
|
||||
v = cherrypy.request.hooks.get(k, [])
|
||||
v.sort()
|
||||
for h in v:
|
||||
msg.append(" %r" % h)
|
||||
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
|
||||
':\n' + '\n'.join(msg), "HTTP")
|
||||
|
||||
def redirect(url='', internal=True):
|
||||
"""Raise InternalRedirect or HTTPRedirect to the given url."""
|
||||
if internal:
|
||||
raise cherrypy.InternalRedirect(url)
|
||||
else:
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
def trailing_slash(missing=True, extra=False):
|
||||
"""Redirect if path_info has (missing|extra) trailing slash."""
|
||||
request = cherrypy.request
|
||||
pi = request.path_info
|
||||
|
||||
if request.is_index is True:
|
||||
if missing:
|
||||
if not pi.endswith('/'):
|
||||
new_url = cherrypy.url(pi + '/', request.query_string)
|
||||
raise cherrypy.HTTPRedirect(new_url)
|
||||
elif request.is_index is False:
|
||||
if extra:
|
||||
# If pi == '/', don't redirect to ''!
|
||||
if pi.endswith('/') and pi != '/':
|
||||
new_url = cherrypy.url(pi[:-1], request.query_string)
|
||||
raise cherrypy.HTTPRedirect(new_url)
|
||||
|
||||
def flatten():
|
||||
"""Wrap response.body in a generator that recursively iterates over body.
|
||||
|
||||
This allows cherrypy.response.body to consist of 'nested generators';
|
||||
that is, a set of generators that yield generators.
|
||||
"""
|
||||
import types
|
||||
def flattener(input):
|
||||
for x in input:
|
||||
if not isinstance(x, types.GeneratorType):
|
||||
yield x
|
||||
else:
|
||||
for y in flattener(x):
|
||||
yield y
|
||||
response = cherrypy.response
|
||||
response.body = flattener(response.body)
|
||||
|
||||
|
||||
def accept(media=None):
|
||||
"""Return the client's preferred media-type (from the given Content-Types).
|
||||
|
||||
If 'media' is None (the default), no test will be performed.
|
||||
|
||||
If 'media' is provided, it should be the Content-Type value (as a string)
|
||||
or values (as a list or tuple of strings) which the current request
|
||||
can emit. The client's acceptable media ranges (as declared in the
|
||||
Accept request header) will be matched in order to these Content-Type
|
||||
values; the first such string is returned. That is, the return value
|
||||
will always be one of the strings provided in the 'media' arg (or None
|
||||
if 'media' is None).
|
||||
|
||||
If no match is found, then HTTPError 406 (Not Acceptable) is raised.
|
||||
Note that most web browsers send */* as a (low-quality) acceptable
|
||||
media range, which should match any Content-Type. In addition, "...if
|
||||
no Accept header field is present, then it is assumed that the client
|
||||
accepts all media types."
|
||||
|
||||
Matching types are checked in order of client preference first,
|
||||
and then in the order of the given 'media' values.
|
||||
|
||||
Note that this function does not honor accept-params (other than "q").
|
||||
"""
|
||||
if not media:
|
||||
return
|
||||
if isinstance(media, basestring):
|
||||
media = [media]
|
||||
|
||||
# Parse the Accept request header, and try to match one
|
||||
# of the requested media-ranges (in order of preference).
|
||||
ranges = cherrypy.request.headers.elements('Accept')
|
||||
if not ranges:
|
||||
# Any media type is acceptable.
|
||||
return media[0]
|
||||
else:
|
||||
# Note that 'ranges' is sorted in order of preference
|
||||
for element in ranges:
|
||||
if element.qvalue > 0:
|
||||
if element.value == "*/*":
|
||||
# Matches any type or subtype
|
||||
return media[0]
|
||||
elif element.value.endswith("/*"):
|
||||
# Matches any subtype
|
||||
mtype = element.value[:-1] # Keep the slash
|
||||
for m in media:
|
||||
if m.startswith(mtype):
|
||||
return m
|
||||
else:
|
||||
# Matches exact value
|
||||
if element.value in media:
|
||||
return element.value
|
||||
|
||||
# No suitable media-range found.
|
||||
ah = cherrypy.request.headers.get('Accept')
|
||||
if ah is None:
|
||||
msg = "Your client did not send an Accept header."
|
||||
else:
|
||||
msg = "Your client sent this Accept header: %s." % ah
|
||||
msg += (" But this resource only emits these media types: %s." %
|
||||
", ".join(media))
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
265
cherrypy/lib/encoding.py
Normal file
@@ -0,0 +1,265 @@
|
||||
import struct
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
def decode(encoding=None, default_encoding='utf-8'):
|
||||
"""Decode cherrypy.request.params from str to unicode objects."""
|
||||
if not encoding:
|
||||
ct = cherrypy.request.headers.elements("Content-Type")
|
||||
if ct:
|
||||
ct = ct[0]
|
||||
encoding = ct.params.get("charset", None)
|
||||
if (not encoding) and ct.value.lower().startswith("text/"):
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
|
||||
# When no explicit charset parameter is provided by the
|
||||
# sender, media subtypes of the "text" type are defined
|
||||
# to have a default charset value of "ISO-8859-1" when
|
||||
# received via HTTP.
|
||||
encoding = "ISO-8859-1"
|
||||
|
||||
if not encoding:
|
||||
encoding = default_encoding
|
||||
|
||||
try:
|
||||
decode_params(encoding)
|
||||
except UnicodeDecodeError:
|
||||
# IE and Firefox don't supply a charset when submitting form
|
||||
# params with a CT of application/x-www-form-urlencoded.
|
||||
# So after all our guessing, it could *still* be wrong.
|
||||
# Start over with ISO-8859-1, since that seems to be preferred.
|
||||
decode_params("ISO-8859-1")
|
||||
|
||||
def decode_params(encoding):
|
||||
decoded_params = {}
|
||||
for key, value in cherrypy.request.params.items():
|
||||
if not hasattr(value, 'file'):
|
||||
# Skip the value if it is an uploaded file
|
||||
if isinstance(value, list):
|
||||
# value is a list: decode each element
|
||||
value = [v.decode(encoding) for v in value]
|
||||
elif isinstance(value, str):
|
||||
# value is a regular string: decode it
|
||||
value = value.decode(encoding)
|
||||
decoded_params[key] = value
|
||||
|
||||
# Decode all or nothing, so we can try again on error.
|
||||
cherrypy.request.params = decoded_params
|
||||
|
||||
|
||||
# Encoding
|
||||
|
||||
def encode(encoding=None, errors='strict', text_only=True, add_charset=True):
|
||||
# Guard against running twice
|
||||
if getattr(cherrypy.request, "_encoding_attempted", False):
|
||||
return
|
||||
cherrypy.request._encoding_attempted = True
|
||||
|
||||
ct = cherrypy.response.headers.elements("Content-Type")
|
||||
if ct:
|
||||
ct = ct[0]
|
||||
if (not text_only) or ct.value.lower().startswith("text/"):
|
||||
# Set "charset=..." param on response Content-Type header
|
||||
ct.params['charset'] = find_acceptable_charset(encoding, errors=errors)
|
||||
if add_charset:
|
||||
cherrypy.response.headers["Content-Type"] = str(ct)
|
||||
|
||||
def encode_stream(encoding, errors='strict'):
|
||||
"""Encode a streaming response body.
|
||||
|
||||
Use a generator wrapper, and just pray it works as the stream is
|
||||
being written out.
|
||||
"""
|
||||
def encoder(body):
|
||||
for chunk in body:
|
||||
if isinstance(chunk, unicode):
|
||||
chunk = chunk.encode(encoding, errors)
|
||||
yield chunk
|
||||
cherrypy.response.body = encoder(cherrypy.response.body)
|
||||
return True
|
||||
|
||||
def encode_string(encoding, errors='strict'):
|
||||
"""Encode a buffered response body."""
|
||||
try:
|
||||
body = []
|
||||
for chunk in cherrypy.response.body:
|
||||
if isinstance(chunk, unicode):
|
||||
chunk = chunk.encode(encoding, errors)
|
||||
body.append(chunk)
|
||||
cherrypy.response.body = body
|
||||
except (LookupError, UnicodeError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def find_acceptable_charset(encoding=None, default_encoding='utf-8', errors='strict'):
|
||||
response = cherrypy.response
|
||||
|
||||
if cherrypy.response.stream:
|
||||
encoder = encode_stream
|
||||
else:
|
||||
response.collapse_body()
|
||||
encoder = encode_string
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
# Encoded strings may be of different lengths from their
|
||||
# unicode equivalents, and even from each other. For example:
|
||||
# >>> t = u"\u7007\u3040"
|
||||
# >>> len(t)
|
||||
# 2
|
||||
# >>> len(t.encode("UTF-8"))
|
||||
# 6
|
||||
# >>> len(t.encode("utf7"))
|
||||
# 8
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
# Parse the Accept-Charset request header, and try to provide one
|
||||
# of the requested charsets (in order of user preference).
|
||||
encs = cherrypy.request.headers.elements('Accept-Charset')
|
||||
charsets = [enc.value.lower() for enc in encs]
|
||||
attempted_charsets = []
|
||||
|
||||
if encoding is not None:
|
||||
# If specified, force this encoding to be used, or fail.
|
||||
encoding = encoding.lower()
|
||||
if (not charsets) or "*" in charsets or encoding in charsets:
|
||||
if encoder(encoding, errors):
|
||||
return encoding
|
||||
else:
|
||||
if not encs:
|
||||
# Any character-set is acceptable.
|
||||
if encoder(default_encoding, errors):
|
||||
return default_encoding
|
||||
else:
|
||||
raise cherrypy.HTTPError(500, failmsg % default_encoding)
|
||||
else:
|
||||
if "*" not in charsets:
|
||||
# If no "*" is present in an Accept-Charset field, then all
|
||||
# character sets not explicitly mentioned get a quality
|
||||
# value of 0, except for ISO-8859-1, which gets a quality
|
||||
# value of 1 if not explicitly mentioned.
|
||||
iso = 'iso-8859-1'
|
||||
if iso not in charsets:
|
||||
attempted_charsets.append(iso)
|
||||
if encoder(iso, errors):
|
||||
return iso
|
||||
|
||||
for element in encs:
|
||||
if element.qvalue > 0:
|
||||
if element.value == "*":
|
||||
# Matches any charset. Try our default.
|
||||
if default_encoding not in attempted_charsets:
|
||||
attempted_charsets.append(default_encoding)
|
||||
if encoder(default_encoding, errors):
|
||||
return default_encoding
|
||||
else:
|
||||
encoding = element.value
|
||||
if encoding not in attempted_charsets:
|
||||
attempted_charsets.append(encoding)
|
||||
if encoder(encoding, errors):
|
||||
return encoding
|
||||
|
||||
# No suitable encoding found.
|
||||
ac = cherrypy.request.headers.get('Accept-Charset')
|
||||
if ac is None:
|
||||
msg = "Your client did not send an Accept-Charset header."
|
||||
else:
|
||||
msg = "Your client sent this Accept-Charset header: %s." % ac
|
||||
msg += " We tried these charsets: %s." % ", ".join(attempted_charsets)
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
|
||||
# GZIP
|
||||
|
||||
def compress(body, compress_level):
|
||||
"""Compress 'body' at the given compress_level."""
|
||||
import zlib
|
||||
|
||||
yield '\037\213' # magic header
|
||||
yield '\010' # compression method
|
||||
yield '\0'
|
||||
yield struct.pack("<L", long(time.time()))
|
||||
yield '\002'
|
||||
yield '\377'
|
||||
|
||||
crc = zlib.crc32("")
|
||||
size = 0
|
||||
zobj = zlib.compressobj(compress_level,
|
||||
zlib.DEFLATED, -zlib.MAX_WBITS,
|
||||
zlib.DEF_MEM_LEVEL, 0)
|
||||
for line in body:
|
||||
size += len(line)
|
||||
crc = zlib.crc32(line, crc)
|
||||
yield zobj.compress(line)
|
||||
yield zobj.flush()
|
||||
yield struct.pack("<l", crc)
|
||||
yield struct.pack("<L", size & 0xFFFFFFFFL)
|
||||
|
||||
def decompress(body):
|
||||
import gzip, StringIO
|
||||
|
||||
zbuf = StringIO.StringIO()
|
||||
zbuf.write(body)
|
||||
zbuf.seek(0)
|
||||
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
|
||||
data = zfile.read()
|
||||
zfile.close()
|
||||
return data
|
||||
|
||||
|
||||
def gzip(compress_level=9, mime_types=['text/html', 'text/plain']):
|
||||
"""Try to gzip the response body if Content-Type in mime_types.
|
||||
|
||||
cherrypy.response.headers['Content-Type'] must be set to one of the
|
||||
values in the mime_types arg before calling this function.
|
||||
|
||||
No compression is performed if any of the following hold:
|
||||
* The client sends no Accept-Encoding request header
|
||||
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
|
||||
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present
|
||||
* The 'identity' value is given with a qvalue > 0.
|
||||
"""
|
||||
response = cherrypy.response
|
||||
if not response.body:
|
||||
# Response body is empty (might be a 304 for instance)
|
||||
return
|
||||
|
||||
# If returning cached content (which should already have been gzipped),
|
||||
# don't re-zip.
|
||||
if getattr(cherrypy.request, "cached", False):
|
||||
return
|
||||
|
||||
acceptable = cherrypy.request.headers.elements('Accept-Encoding')
|
||||
if not acceptable:
|
||||
# If no Accept-Encoding field is present in a request,
|
||||
# the server MAY assume that the client will accept any
|
||||
# content coding. In this case, if "identity" is one of
|
||||
# the available content-codings, then the server SHOULD use
|
||||
# the "identity" content-coding, unless it has additional
|
||||
# information that a different content-coding is meaningful
|
||||
# to the client.
|
||||
return
|
||||
|
||||
ct = response.headers.get('Content-Type', '').split(';')[0]
|
||||
for coding in acceptable:
|
||||
if coding.value == 'identity' and coding.qvalue != 0:
|
||||
return
|
||||
if coding.value in ('gzip', 'x-gzip'):
|
||||
if coding.qvalue == 0:
|
||||
return
|
||||
if ct in mime_types:
|
||||
# Return a generator that compresses the page
|
||||
varies = response.headers.get("Vary", "")
|
||||
varies = [x.strip() for x in varies.split(",") if x.strip()]
|
||||
if "Accept-Encoding" not in varies:
|
||||
varies.append("Accept-Encoding")
|
||||
response.headers['Vary'] = ", ".join(varies)
|
||||
|
||||
response.headers['Content-Encoding'] = 'gzip'
|
||||
response.body = compress(response.body, compress_level)
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
return
|
||||
cherrypy.HTTPError(406, "identity, gzip").set_response()
|
||||
410
cherrypy/lib/http.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""HTTP library functions."""
|
||||
|
||||
# This module contains functions for building an HTTP application
|
||||
# framework: any one, not just one whose name starts with "Ch". ;) If you
|
||||
# reference any modules from some popular framework inside *this* module,
|
||||
# FuManChu will personally hang you up by your thumbs and submit you
|
||||
# to a public caning.
|
||||
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
response_codes = BaseHTTPRequestHandler.responses.copy()
|
||||
|
||||
# From http://www.cherrypy.org/ticket/361
|
||||
response_codes[500] = ('Internal Server Error',
|
||||
'The server encountered an unexpected condition '
|
||||
'which prevented it from fulfilling the request.')
|
||||
response_codes[503] = ('Service Unavailable',
|
||||
'The server is currently unable to handle the '
|
||||
'request due to a temporary overloading or '
|
||||
'maintenance of the server.')
|
||||
|
||||
|
||||
import cgi
|
||||
import re
|
||||
from rfc822 import formatdate as HTTPDate
|
||||
|
||||
|
||||
def urljoin(*atoms):
|
||||
"""Return the given path *atoms, joined into a single URL.
|
||||
|
||||
This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
||||
original URL, even if either atom is blank.
|
||||
"""
|
||||
url = "/".join([x for x in atoms if x])
|
||||
while "//" in url:
|
||||
url = url.replace("//", "/")
|
||||
# Special-case the final url of "", and return "/" instead.
|
||||
return url or "/"
|
||||
|
||||
def protocol_from_http(protocol_str):
|
||||
"""Return a protocol tuple from the given 'HTTP/x.y' string."""
|
||||
return int(protocol_str[5]), int(protocol_str[7])
|
||||
|
||||
def get_ranges(headervalue, content_length):
|
||||
"""Return a list of (start, stop) indices from a Range header, or None.
|
||||
|
||||
Each (start, stop) tuple will be composed of two ints, which are suitable
|
||||
for use in a slicing operation. That is, the header "Range: bytes=3-6",
|
||||
if applied against a Python string, is requesting resource[3:7]. This
|
||||
function will return the list [(3, 7)].
|
||||
|
||||
If this function returns an empty list, you should return HTTP 416.
|
||||
"""
|
||||
|
||||
if not headervalue:
|
||||
return None
|
||||
|
||||
result = []
|
||||
bytesunit, byteranges = headervalue.split("=", 1)
|
||||
for brange in byteranges.split(","):
|
||||
start, stop = [x.strip() for x in brange.split("-", 1)]
|
||||
if start:
|
||||
if not stop:
|
||||
stop = content_length - 1
|
||||
start, stop = map(int, (start, stop))
|
||||
if start >= content_length:
|
||||
# From rfc 2616 sec 14.16:
|
||||
# "If the server receives a request (other than one
|
||||
# including an If-Range request-header field) with an
|
||||
# unsatisfiable Range request-header field (that is,
|
||||
# all of whose byte-range-spec values have a first-byte-pos
|
||||
# value greater than the current length of the selected
|
||||
# resource), it SHOULD return a response code of 416
|
||||
# (Requested range not satisfiable)."
|
||||
continue
|
||||
if stop < start:
|
||||
# From rfc 2616 sec 14.16:
|
||||
# "If the server ignores a byte-range-spec because it
|
||||
# is syntactically invalid, the server SHOULD treat
|
||||
# the request as if the invalid Range header field
|
||||
# did not exist. (Normally, this means return a 200
|
||||
# response containing the full entity)."
|
||||
return None
|
||||
result.append((start, stop + 1))
|
||||
else:
|
||||
if not stop:
|
||||
# See rfc quote above.
|
||||
return None
|
||||
# Negative subscript (last N bytes)
|
||||
result.append((content_length - int(stop), content_length))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class HeaderElement(object):
|
||||
"""An element (with parameters) from an HTTP header's element list."""
|
||||
|
||||
def __init__(self, value, params=None):
|
||||
self.value = value
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
def __unicode__(self):
|
||||
p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()]
|
||||
return u"%s%s" % (self.value, "".join(p))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
def parse(elementstr):
|
||||
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
|
||||
# Split the element into a value and parameters. The 'value' may
|
||||
# be of the form, "token=token", but we don't split that here.
|
||||
atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
|
||||
initial_value = atoms.pop(0).strip()
|
||||
params = {}
|
||||
for atom in atoms:
|
||||
atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
|
||||
key = atom.pop(0)
|
||||
if atom:
|
||||
val = atom[0]
|
||||
else:
|
||||
val = ""
|
||||
params[key] = val
|
||||
return initial_value, params
|
||||
parse = staticmethod(parse)
|
||||
|
||||
def from_str(cls, elementstr):
|
||||
"""Construct an instance from a string of the form 'token;key=val'."""
|
||||
ival, params = cls.parse(elementstr)
|
||||
return cls(ival, params)
|
||||
from_str = classmethod(from_str)
|
||||
|
||||
|
||||
q_separator = re.compile(r'; *q *=')
|
||||
|
||||
class AcceptElement(HeaderElement):
|
||||
"""An element (with parameters) from an Accept* header's element list.
|
||||
|
||||
AcceptElement objects are comparable; the more-preferred object will be
|
||||
"less than" the less-preferred object. They are also therefore sortable;
|
||||
if you sort a list of AcceptElement objects, they will be listed in
|
||||
priority order; the most preferred value will be first. Yes, it should
|
||||
have been the other way around, but it's too late to fix now.
|
||||
"""
|
||||
|
||||
def from_str(cls, elementstr):
|
||||
qvalue = None
|
||||
# The first "q" parameter (if any) separates the initial
|
||||
# media-range parameter(s) (if any) from the accept-params.
|
||||
atoms = q_separator.split(elementstr, 1)
|
||||
media_range = atoms.pop(0).strip()
|
||||
if atoms:
|
||||
# The qvalue for an Accept header can have extensions. The other
|
||||
# headers cannot, but it's easier to parse them as if they did.
|
||||
qvalue = HeaderElement.from_str(atoms[0].strip())
|
||||
|
||||
media_type, params = cls.parse(media_range)
|
||||
if qvalue is not None:
|
||||
params["q"] = qvalue
|
||||
return cls(media_type, params)
|
||||
from_str = classmethod(from_str)
|
||||
|
||||
def qvalue(self):
|
||||
val = self.params.get("q", "1")
|
||||
if isinstance(val, HeaderElement):
|
||||
val = val.value
|
||||
return float(val)
|
||||
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
|
||||
|
||||
def __cmp__(self, other):
|
||||
diff = cmp(other.qvalue, self.qvalue)
|
||||
if diff == 0:
|
||||
diff = cmp(str(other), str(self))
|
||||
return diff
|
||||
|
||||
|
||||
def header_elements(fieldname, fieldvalue):
|
||||
"""Return a HeaderElement list from a comma-separated header str."""
|
||||
|
||||
if not fieldvalue:
|
||||
return None
|
||||
headername = fieldname.lower()
|
||||
|
||||
result = []
|
||||
for element in fieldvalue.split(","):
|
||||
if headername.startswith("accept") or headername == 'te':
|
||||
hv = AcceptElement.from_str(element)
|
||||
else:
|
||||
hv = HeaderElement.from_str(element)
|
||||
result.append(hv)
|
||||
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
def decode_TEXT(value):
|
||||
"""Decode RFC-2047 TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
|
||||
from email.Header import decode_header
|
||||
atoms = decode_header(value)
|
||||
decodedvalue = ""
|
||||
for atom, charset in atoms:
|
||||
if charset is not None:
|
||||
atom = atom.decode(charset)
|
||||
decodedvalue += atom
|
||||
return decodedvalue
|
||||
|
||||
def valid_status(status):
|
||||
"""Return legal HTTP status Code, Reason-phrase and Message.
|
||||
|
||||
The status arg must be an int, or a str that begins with an int.
|
||||
|
||||
If status is an int, or a str and no reason-phrase is supplied,
|
||||
a default reason-phrase will be provided.
|
||||
"""
|
||||
|
||||
if not status:
|
||||
status = 200
|
||||
|
||||
status = str(status)
|
||||
parts = status.split(" ", 1)
|
||||
if len(parts) == 1:
|
||||
# No reason supplied.
|
||||
code, = parts
|
||||
reason = None
|
||||
else:
|
||||
code, reason = parts
|
||||
reason = reason.strip()
|
||||
|
||||
try:
|
||||
code = int(code)
|
||||
except ValueError:
|
||||
raise ValueError("Illegal response status from server "
|
||||
"(%s is non-numeric)." % repr(code))
|
||||
|
||||
if code < 100 or code > 599:
|
||||
raise ValueError("Illegal response status from server "
|
||||
"(%s is out of range)." % repr(code))
|
||||
|
||||
if code not in response_codes:
|
||||
# code is unknown but not illegal
|
||||
default_reason, message = "", ""
|
||||
else:
|
||||
default_reason, message = response_codes[code]
|
||||
|
||||
if reason is None:
|
||||
reason = default_reason
|
||||
|
||||
return code, reason, message
|
||||
|
||||
|
||||
image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
|
||||
|
||||
def parse_query_string(query_string, keep_blank_values=True):
|
||||
"""Build a params dictionary from a query_string.
|
||||
|
||||
Duplicate key/value pairs in the provided query_string will be
|
||||
returned as {'key': [val1, val2, ...]}. Single key/values will
|
||||
be returned as strings: {'key': 'value'}.
|
||||
"""
|
||||
if image_map_pattern.match(query_string):
|
||||
# Server-side image map. Map the coords to 'x' and 'y'
|
||||
# (like CGI::Request does).
|
||||
pm = query_string.split(",")
|
||||
pm = {'x': int(pm[0]), 'y': int(pm[1])}
|
||||
else:
|
||||
pm = cgi.parse_qs(query_string, keep_blank_values)
|
||||
for key, val in pm.items():
|
||||
if len(val) == 1:
|
||||
pm[key] = val[0]
|
||||
return pm
|
||||
|
||||
def params_from_CGI_form(form):
|
||||
params = {}
|
||||
for key in form.keys():
|
||||
value_list = form[key]
|
||||
if key is None:
|
||||
# multipart/* message parts that have no Content-Disposition
|
||||
# have a .name of None, but Python kwarg keys must be strings.
|
||||
# See http://www.cherrypy.org/ticket/890.
|
||||
key = 'parts'
|
||||
if isinstance(value_list, list):
|
||||
params[key] = []
|
||||
for item in value_list:
|
||||
if item.filename is not None:
|
||||
value = item # It's a file upload
|
||||
else:
|
||||
value = item.value # It's a regular field
|
||||
params[key].append(value)
|
||||
else:
|
||||
if value_list.filename is not None:
|
||||
value = value_list # It's a file upload
|
||||
else:
|
||||
value = value_list.value # It's a regular field
|
||||
params[key] = value
|
||||
return params
|
||||
|
||||
|
||||
class CaseInsensitiveDict(dict):
|
||||
"""A case-insensitive dict subclass.
|
||||
|
||||
Each key is changed on entry to str(key).title().
|
||||
"""
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, str(key).title())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, str(key).title(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, str(key).title())
|
||||
|
||||
def __contains__(self, key):
|
||||
return dict.__contains__(self, str(key).title())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return dict.get(self, str(key).title(), default)
|
||||
|
||||
def has_key(self, key):
|
||||
return dict.has_key(self, str(key).title())
|
||||
|
||||
def update(self, E):
|
||||
for k in E.keys():
|
||||
self[str(k).title()] = E[k]
|
||||
|
||||
def fromkeys(cls, seq, value=None):
|
||||
newdict = cls()
|
||||
for k in seq:
|
||||
newdict[str(k).title()] = value
|
||||
return newdict
|
||||
fromkeys = classmethod(fromkeys)
|
||||
|
||||
def setdefault(self, key, x=None):
|
||||
key = str(key).title()
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
self[key] = x
|
||||
return x
|
||||
|
||||
def pop(self, key, default):
|
||||
return dict.pop(self, str(key).title(), default)
|
||||
|
||||
|
||||
class HeaderMap(CaseInsensitiveDict):
|
||||
"""A dict subclass for HTTP request and response headers.
|
||||
|
||||
Each key is changed on entry to str(key).title(). This allows headers
|
||||
to be case-insensitive and avoid duplicates.
|
||||
|
||||
Values are header values (decoded according to RFC 2047 if necessary).
|
||||
"""
|
||||
|
||||
def elements(self, key):
|
||||
"""Return a list of HeaderElements for the given header (or None)."""
|
||||
key = str(key).title()
|
||||
h = self.get(key)
|
||||
if h is None:
|
||||
return []
|
||||
return header_elements(key, h)
|
||||
|
||||
def output(self, protocol=(1, 1)):
|
||||
"""Transform self into a list of (name, value) tuples."""
|
||||
header_list = []
|
||||
for key, v in self.iteritems():
|
||||
if isinstance(v, unicode):
|
||||
# HTTP/1.0 says, "Words of *TEXT may contain octets
|
||||
# from character sets other than US-ASCII." and
|
||||
# "Recipients of header field TEXT containing octets
|
||||
# outside the US-ASCII character set may assume that
|
||||
# they represent ISO-8859-1 characters."
|
||||
try:
|
||||
v = v.encode("iso-8859-1")
|
||||
except UnicodeEncodeError:
|
||||
if protocol >= (1, 1):
|
||||
# Encode RFC-2047 TEXT
|
||||
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
|
||||
from email.Header import Header
|
||||
v = Header(v, 'utf-8').encode()
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# This coercion should not take any time at all
|
||||
# if value is already of type "str".
|
||||
v = str(v)
|
||||
header_list.append((key, v))
|
||||
return header_list
|
||||
|
||||
|
||||
|
||||
class Host(object):
|
||||
"""An internet address.
|
||||
|
||||
name should be the client's host name. If not available (because no DNS
|
||||
lookup is performed), the IP address should be used instead.
|
||||
"""
|
||||
|
||||
ip = "0.0.0.0"
|
||||
port = 80
|
||||
name = "unknown.tld"
|
||||
|
||||
def __init__(self, ip, port, name=None):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
if name is None:
|
||||
name = ip
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return "http.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
|
||||
361
cherrypy/lib/httpauth.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
httpauth modules defines functions to implement HTTP Digest Authentication (RFC 2617).
|
||||
This has full compliance with 'Digest' and 'Basic' authentication methods. In
|
||||
'Digest' it supports both MD5 and MD5-sess algorithms.
|
||||
|
||||
Usage:
|
||||
|
||||
First use 'doAuth' to request the client authentication for a
|
||||
certain resource. You should send an httplib.UNAUTHORIZED response to the
|
||||
client so he knows he has to authenticate itself.
|
||||
|
||||
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
|
||||
'checkResponse'.
|
||||
|
||||
To use 'checkResponse' you must have already verified the password associated
|
||||
with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse'
|
||||
function to verify if the password matches the one sent by the client.
|
||||
|
||||
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
|
||||
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
|
||||
"""
|
||||
__version__ = 1, 0, 1
|
||||
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
|
||||
__credits__ = """
|
||||
Peter van Kampen for its recipe which implement most of Digest authentication:
|
||||
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
|
||||
"""
|
||||
|
||||
__license__ = """
|
||||
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of Sylvain Hellegouarch nor the names of his contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
|
||||
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
|
||||
"calculateNonce", "SUPPORTED_QOP")
|
||||
|
||||
################################################################################
|
||||
try:
|
||||
# Python 2.5+
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import new as md5
|
||||
import time
|
||||
import base64
|
||||
import urllib2
|
||||
|
||||
MD5 = "MD5"
|
||||
MD5_SESS = "MD5-sess"
|
||||
AUTH = "auth"
|
||||
AUTH_INT = "auth-int"
|
||||
|
||||
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
|
||||
SUPPORTED_QOP = (AUTH, AUTH_INT)
|
||||
|
||||
################################################################################
|
||||
# doAuth
|
||||
#
|
||||
DIGEST_AUTH_ENCODERS = {
|
||||
MD5: lambda val: md5(val).hexdigest(),
|
||||
MD5_SESS: lambda val: md5(val).hexdigest(),
|
||||
# SHA: lambda val: sha.new (val).hexdigest (),
|
||||
}
|
||||
|
||||
def calculateNonce (realm, algorithm = MD5):
|
||||
"""This is an auxaliary function that calculates 'nonce' value. It is used
|
||||
to handle sessions."""
|
||||
|
||||
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
|
||||
assert algorithm in SUPPORTED_ALGORITHM
|
||||
|
||||
try:
|
||||
encoder = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
except KeyError:
|
||||
raise NotImplementedError ("The chosen algorithm (%s) does not have "\
|
||||
"an implementation yet" % algorithm)
|
||||
|
||||
return encoder ("%d:%s" % (time.time(), realm))
|
||||
|
||||
def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
|
||||
"""Challenges the client for a Digest authentication."""
|
||||
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
|
||||
assert algorithm in SUPPORTED_ALGORITHM
|
||||
assert qop in SUPPORTED_QOP
|
||||
|
||||
if nonce is None:
|
||||
nonce = calculateNonce (realm, algorithm)
|
||||
|
||||
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
|
||||
realm, nonce, algorithm, qop
|
||||
)
|
||||
|
||||
def basicAuth (realm):
|
||||
"""Challengenes the client for a Basic authentication."""
|
||||
assert '"' not in realm, "Realms cannot contain the \" (quote) character."
|
||||
|
||||
return 'Basic realm="%s"' % realm
|
||||
|
||||
def doAuth (realm):
|
||||
"""'doAuth' function returns the challenge string b giving priority over
|
||||
Digest and fallback to Basic authentication when the browser doesn't
|
||||
support the first one.
|
||||
|
||||
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
|
||||
|
||||
return digestAuth (realm) + " " + basicAuth (realm)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Parse authorization parameters
|
||||
#
|
||||
def _parseDigestAuthorization (auth_params):
|
||||
# Convert the auth params to a dict
|
||||
items = urllib2.parse_http_list (auth_params)
|
||||
params = urllib2.parse_keqv_list (items)
|
||||
|
||||
# Now validate the params
|
||||
|
||||
# Check for required parameters
|
||||
required = ["username", "realm", "nonce", "uri", "response"]
|
||||
for k in required:
|
||||
if not params.has_key(k):
|
||||
return None
|
||||
|
||||
# If qop is sent then cnonce and nc MUST be present
|
||||
if params.has_key("qop") and not (params.has_key("cnonce") \
|
||||
and params.has_key("nc")):
|
||||
return None
|
||||
|
||||
# If qop is not sent, neither cnonce nor nc can be present
|
||||
if (params.has_key("cnonce") or params.has_key("nc")) and \
|
||||
not params.has_key("qop"):
|
||||
return None
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def _parseBasicAuthorization (auth_params):
|
||||
username, password = base64.decodestring (auth_params).split (":", 1)
|
||||
return {"username": username, "password": password}
|
||||
|
||||
AUTH_SCHEMES = {
|
||||
"basic": _parseBasicAuthorization,
|
||||
"digest": _parseDigestAuthorization,
|
||||
}
|
||||
|
||||
def parseAuthorization (credentials):
|
||||
"""parseAuthorization will convert the value of the 'Authorization' key in
|
||||
the HTTP header to a map itself. If the parsing fails 'None' is returned.
|
||||
"""
|
||||
|
||||
global AUTH_SCHEMES
|
||||
|
||||
auth_scheme, auth_params = credentials.split(" ", 1)
|
||||
auth_scheme = auth_scheme.lower ()
|
||||
|
||||
parser = AUTH_SCHEMES[auth_scheme]
|
||||
params = parser (auth_params)
|
||||
|
||||
if params is None:
|
||||
return
|
||||
|
||||
assert "auth_scheme" not in params
|
||||
params["auth_scheme"] = auth_scheme
|
||||
return params
|
||||
|
||||
|
||||
################################################################################
|
||||
# Check provided response for a valid password
|
||||
#
|
||||
def md5SessionKey (params, password):
|
||||
"""
|
||||
If the "algorithm" directive's value is "MD5-sess", then A1
|
||||
[the session key] is calculated only once - on the first request by the
|
||||
client following receipt of a WWW-Authenticate challenge from the server.
|
||||
|
||||
This creates a 'session key' for the authentication of subsequent
|
||||
requests and responses which is different for each "authentication
|
||||
session", thus limiting the amount of material hashed with any one
|
||||
key.
|
||||
|
||||
Because the server need only use the hash of the user
|
||||
credentials in order to create the A1 value, this construction could
|
||||
be used in conjunction with a third party authentication service so
|
||||
that the web server would not need the actual password value. The
|
||||
specification of such a protocol is beyond the scope of this
|
||||
specification.
|
||||
"""
|
||||
|
||||
keys = ("username", "realm", "nonce", "cnonce")
|
||||
params_copy = {}
|
||||
for key in keys:
|
||||
params_copy[key] = params[key]
|
||||
|
||||
params_copy["algorithm"] = MD5_SESS
|
||||
return _A1 (params_copy, password)
|
||||
|
||||
def _A1(params, password):
|
||||
algorithm = params.get ("algorithm", MD5)
|
||||
H = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
|
||||
if algorithm == MD5:
|
||||
# If the "algorithm" directive's value is "MD5" or is
|
||||
# unspecified, then A1 is:
|
||||
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
|
||||
return "%s:%s:%s" % (params["username"], params["realm"], password)
|
||||
|
||||
elif algorithm == MD5_SESS:
|
||||
|
||||
# This is A1 if qop is set
|
||||
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
|
||||
# ":" unq(nonce-value) ":" unq(cnonce-value)
|
||||
h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
|
||||
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
|
||||
|
||||
|
||||
def _A2(params, method, kwargs):
|
||||
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
|
||||
# A2 = Method ":" digest-uri-value
|
||||
|
||||
qop = params.get ("qop", "auth")
|
||||
if qop == "auth":
|
||||
return method + ":" + params["uri"]
|
||||
elif qop == "auth-int":
|
||||
# If the "qop" value is "auth-int", then A2 is:
|
||||
# A2 = Method ":" digest-uri-value ":" H(entity-body)
|
||||
entity_body = kwargs.get ("entity_body", "")
|
||||
H = kwargs["H"]
|
||||
|
||||
return "%s:%s:%s" % (
|
||||
method,
|
||||
params["uri"],
|
||||
H(entity_body)
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
|
||||
|
||||
def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
|
||||
"""
|
||||
Generates a response respecting the algorithm defined in RFC 2617
|
||||
"""
|
||||
params = auth_map
|
||||
|
||||
algorithm = params.get ("algorithm", MD5)
|
||||
|
||||
H = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
KD = lambda secret, data: H(secret + ":" + data)
|
||||
|
||||
qop = params.get ("qop", None)
|
||||
|
||||
H_A2 = H(_A2(params, method, kwargs))
|
||||
|
||||
if algorithm == MD5_SESS and A1 is not None:
|
||||
H_A1 = H(A1)
|
||||
else:
|
||||
H_A1 = H(_A1(params, password))
|
||||
|
||||
if qop in ("auth", "auth-int"):
|
||||
# If the "qop" value is "auth" or "auth-int":
|
||||
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
|
||||
# ":" nc-value
|
||||
# ":" unq(cnonce-value)
|
||||
# ":" unq(qop-value)
|
||||
# ":" H(A2)
|
||||
# ) <">
|
||||
request = "%s:%s:%s:%s:%s" % (
|
||||
params["nonce"],
|
||||
params["nc"],
|
||||
params["cnonce"],
|
||||
params["qop"],
|
||||
H_A2,
|
||||
)
|
||||
elif qop is None:
|
||||
# If the "qop" directive is not present (this construction is
|
||||
# for compatibility with RFC 2069):
|
||||
# request-digest =
|
||||
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
|
||||
request = "%s:%s" % (params["nonce"], H_A2)
|
||||
|
||||
return KD(H_A1, request)
|
||||
|
||||
def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
|
||||
"""This function is used to verify the response given by the client when
|
||||
he tries to authenticate.
|
||||
Optional arguments:
|
||||
entity_body - when 'qop' is set to 'auth-int' you MUST provide the
|
||||
raw data you are going to send to the client (usually the
|
||||
HTML page.
|
||||
request_uri - the uri from the request line compared with the 'uri'
|
||||
directive of the authorization map. They must represent
|
||||
the same resource (unused at this time).
|
||||
"""
|
||||
|
||||
if auth_map['realm'] != kwargs.get('realm', None):
|
||||
return False
|
||||
|
||||
response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
|
||||
|
||||
return response == auth_map["response"]
|
||||
|
||||
def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
|
||||
# Note that the Basic response doesn't provide the realm value so we cannot
|
||||
# test it
|
||||
try:
|
||||
return encrypt(auth_map["password"], auth_map["username"]) == password
|
||||
except TypeError:
|
||||
return encrypt(auth_map["password"]) == password
|
||||
|
||||
AUTH_RESPONSES = {
|
||||
"basic": _checkBasicResponse,
|
||||
"digest": _checkDigestResponse,
|
||||
}
|
||||
|
||||
def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
|
||||
"""'checkResponse' compares the auth_map with the password and optionally
|
||||
other arguments that each implementation might need.
|
||||
|
||||
If the response is of type 'Basic' then the function has the following
|
||||
signature:
|
||||
|
||||
checkBasicResponse (auth_map, password) -> bool
|
||||
|
||||
If the response is of type 'Digest' then the function has the following
|
||||
signature:
|
||||
|
||||
checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
|
||||
|
||||
The 'A1' argument is only used in MD5_SESS algorithm based responses.
|
||||
Check md5SessionKey() for more info.
|
||||
"""
|
||||
global AUTH_RESPONSES
|
||||
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
|
||||
return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
198
cherrypy/lib/profiler.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Profiler tools for CherryPy.
|
||||
|
||||
CherryPy users
|
||||
==============
|
||||
|
||||
You can profile any of your pages as follows:
|
||||
|
||||
from cherrypy.lib import profiler
|
||||
|
||||
class Root:
|
||||
p = profile.Profiler("/path/to/profile/dir")
|
||||
|
||||
def index(self):
|
||||
self.p.run(self._index)
|
||||
index.exposed = True
|
||||
|
||||
def _index(self):
|
||||
return "Hello, world!"
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
|
||||
You can also turn on profiling for all requests
|
||||
using the make_app function as WSGI middleware.
|
||||
|
||||
|
||||
CherryPy developers
|
||||
===================
|
||||
|
||||
This module can be used whenever you make changes to CherryPy,
|
||||
to get a quick sanity-check on overall CP performance. Use the
|
||||
"--profile" flag when running the test suite. Then, use the serve()
|
||||
function to browse the results in a web browser. If you run this
|
||||
module from the command line, it will call serve() for you.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Make profiler output more readable by adding __init__ modules' parents.
|
||||
def new_func_strip_path(func_name):
|
||||
filename, line, name = func_name
|
||||
if filename.endswith("__init__.py"):
|
||||
return os.path.basename(filename[:-12]) + filename[-12:], line, name
|
||||
return os.path.basename(filename), line, name
|
||||
|
||||
try:
|
||||
import profile
|
||||
import pstats
|
||||
pstats.func_strip_path = new_func_strip_path
|
||||
except ImportError:
|
||||
profile = None
|
||||
pstats = None
|
||||
import warnings
|
||||
msg = ("Your installation of Python does not have a profile module. "
|
||||
"If you're on Debian, try `sudo apt-get install python-profiler`. "
|
||||
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
|
||||
warnings.warn(msg)
|
||||
|
||||
import os, os.path
|
||||
import sys
|
||||
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
|
||||
_count = 0
|
||||
|
||||
class Profiler(object):
|
||||
|
||||
def __init__(self, path=None):
|
||||
if not path:
|
||||
path = os.path.join(os.path.dirname(__file__), "profile")
|
||||
self.path = path
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def run(self, func, *args, **params):
|
||||
"""Dump profile data into self.path."""
|
||||
global _count
|
||||
c = _count = _count + 1
|
||||
path = os.path.join(self.path, "cp_%04d.prof" % c)
|
||||
prof = profile.Profile()
|
||||
result = prof.runcall(func, *args, **params)
|
||||
prof.dump_stats(path)
|
||||
return result
|
||||
|
||||
def statfiles(self):
|
||||
"""statfiles() -> list of available profiles."""
|
||||
return [f for f in os.listdir(self.path)
|
||||
if f.startswith("cp_") and f.endswith(".prof")]
|
||||
|
||||
def stats(self, filename, sortby='cumulative'):
|
||||
"""stats(index) -> output of print_stats() for the given profile."""
|
||||
sio = StringIO.StringIO()
|
||||
if sys.version_info >= (2, 5):
|
||||
s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
|
||||
s.strip_dirs()
|
||||
s.sort_stats(sortby)
|
||||
s.print_stats()
|
||||
else:
|
||||
# pstats.Stats before Python 2.5 didn't take a 'stream' arg,
|
||||
# but just printed to stdout. So re-route stdout.
|
||||
s = pstats.Stats(os.path.join(self.path, filename))
|
||||
s.strip_dirs()
|
||||
s.sort_stats(sortby)
|
||||
oldout = sys.stdout
|
||||
try:
|
||||
sys.stdout = sio
|
||||
s.print_stats()
|
||||
finally:
|
||||
sys.stdout = oldout
|
||||
response = sio.getvalue()
|
||||
sio.close()
|
||||
return response
|
||||
|
||||
def index(self):
|
||||
return """<html>
|
||||
<head><title>CherryPy profile data</title></head>
|
||||
<frameset cols='200, 1*'>
|
||||
<frame src='menu' />
|
||||
<frame name='main' src='' />
|
||||
</frameset>
|
||||
</html>
|
||||
"""
|
||||
index.exposed = True
|
||||
|
||||
def menu(self):
|
||||
yield "<h2>Profiling runs</h2>"
|
||||
yield "<p>Click on one of the runs below to see profiling data.</p>"
|
||||
runs = self.statfiles()
|
||||
runs.sort()
|
||||
for i in runs:
|
||||
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
|
||||
menu.exposed = True
|
||||
|
||||
def report(self, filename):
|
||||
import cherrypy
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
return self.stats(filename)
|
||||
report.exposed = True
|
||||
|
||||
|
||||
class ProfileAggregator(Profiler):
|
||||
|
||||
def __init__(self, path=None):
|
||||
Profiler.__init__(self, path)
|
||||
global _count
|
||||
self.count = _count = _count + 1
|
||||
self.profiler = profile.Profile()
|
||||
|
||||
def run(self, func, *args):
|
||||
path = os.path.join(self.path, "cp_%04d.prof" % self.count)
|
||||
result = self.profiler.runcall(func, *args)
|
||||
self.profiler.dump_stats(path)
|
||||
return result
|
||||
|
||||
|
||||
class make_app:
|
||||
def __init__(self, nextapp, path=None, aggregate=False):
|
||||
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
|
||||
|
||||
nextapp: the WSGI application to wrap, usually an instance of
|
||||
cherrypy.Application.
|
||||
path: where to dump the profiling output.
|
||||
aggregate: if True, profile data for all HTTP requests will go in
|
||||
a single file. If False (the default), each HTTP request will
|
||||
dump its profile data into a separate file.
|
||||
"""
|
||||
self.nextapp = nextapp
|
||||
self.aggregate = aggregate
|
||||
if aggregate:
|
||||
self.profiler = ProfileAggregator(path)
|
||||
else:
|
||||
self.profiler = Profiler(path)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
def gather():
|
||||
result = []
|
||||
for line in self.nextapp(environ, start_response):
|
||||
result.append(line)
|
||||
return result
|
||||
return self.profiler.run(gather)
|
||||
|
||||
|
||||
def serve(path=None, port=8080):
|
||||
import cherrypy
|
||||
cherrypy.config.update({'server.socket_port': int(port),
|
||||
'server.thread_pool': 10,
|
||||
'environment': "production",
|
||||
})
|
||||
cherrypy.quickstart(Profiler(path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
serve(*tuple(sys.argv[1:]))
|
||||
|
||||
128
cherrypy/lib/safemime.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import cherrypy
|
||||
|
||||
|
||||
class MultipartWrapper(object):
|
||||
"""Wraps a file-like object, returning '' when Content-Length is reached.
|
||||
|
||||
The cgi module's logic for reading multipart MIME messages doesn't
|
||||
allow the parts to know when the Content-Length for the entire message
|
||||
has been reached, and doesn't allow for multipart-MIME messages that
|
||||
omit the trailing CRLF (Flash 8's FileReference.upload(url), for example,
|
||||
does this). The read_lines_to_outerboundary function gets stuck in a loop
|
||||
until the socket times out.
|
||||
|
||||
This rfile wrapper simply monitors the incoming stream. When a read is
|
||||
attempted past the Content-Length, it returns an empty string rather
|
||||
than timing out (of course, if the last read *overlaps* the C-L, you'll
|
||||
get the last bit of data up to C-L, and then the next read will return
|
||||
an empty string).
|
||||
"""
|
||||
|
||||
def __init__(self, rfile, clen):
|
||||
self.rfile = rfile
|
||||
self.clen = clen
|
||||
self.bytes_read = 0
|
||||
|
||||
def read(self, size = None):
|
||||
if self.clen:
|
||||
# Return '' if we've read all the data.
|
||||
if self.bytes_read >= self.clen:
|
||||
return ''
|
||||
|
||||
# Reduce 'size' if it's over our limit.
|
||||
new_bytes_read = self.bytes_read + size
|
||||
if new_bytes_read > self.clen:
|
||||
size = self.clen - self.bytes_read
|
||||
|
||||
data = self.rfile.read(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
def readline(self, size = None):
|
||||
if size is not None:
|
||||
if self.clen:
|
||||
# Return '' if we've read all the data.
|
||||
if self.bytes_read >= self.clen:
|
||||
return ''
|
||||
|
||||
# Reduce 'size' if it's over our limit.
|
||||
new_bytes_read = self.bytes_read + size
|
||||
if new_bytes_read > self.clen:
|
||||
size = self.clen - self.bytes_read
|
||||
|
||||
data = self.rfile.readline(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
# User didn't specify a size ...
|
||||
# We read the line in chunks to make sure it's not a 100MB line !
|
||||
res = []
|
||||
size = 256
|
||||
while True:
|
||||
if self.clen:
|
||||
# Return if we've read all the data.
|
||||
if self.bytes_read >= self.clen:
|
||||
return ''.join(res)
|
||||
|
||||
# Reduce 'size' if it's over our limit.
|
||||
new_bytes_read = self.bytes_read + size
|
||||
if new_bytes_read > self.clen:
|
||||
size = self.clen - self.bytes_read
|
||||
|
||||
data = self.rfile.readline(size)
|
||||
self.bytes_read += len(data)
|
||||
res.append(data)
|
||||
# See http://www.cherrypy.org/ticket/421
|
||||
if len(data) < size or data[-1:] == "\n":
|
||||
return ''.join(res)
|
||||
|
||||
def readlines(self, sizehint = 0):
|
||||
# Shamelessly stolen from StringIO
|
||||
total = 0
|
||||
lines = []
|
||||
line = self.readline()
|
||||
while line:
|
||||
lines.append(line)
|
||||
total += len(line)
|
||||
if 0 < sizehint <= total:
|
||||
break
|
||||
line = self.readline()
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
self.rfile.close()
|
||||
|
||||
def __iter__(self):
|
||||
return self.rfile
|
||||
|
||||
def next(self):
|
||||
if self.clen:
|
||||
# Return '' if we've read all the data.
|
||||
if self.bytes_read >= self.clen:
|
||||
return ''
|
||||
|
||||
data = self.rfile.next()
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
|
||||
def safe_multipart(flash_only=False):
|
||||
"""Wrap request.rfile in a reader that won't crash on no trailing CRLF."""
|
||||
h = cherrypy.request.headers
|
||||
if not h.get('Content-Type','').startswith('multipart/'):
|
||||
return
|
||||
if flash_only and not 'Shockwave Flash' in h.get('User-Agent', ''):
|
||||
return
|
||||
|
||||
clen = h.get('Content-Length', '0')
|
||||
try:
|
||||
clen = int(clen)
|
||||
except ValueError:
|
||||
return
|
||||
cherrypy.request.rfile = MultipartWrapper(cherrypy.request.rfile, clen)
|
||||
|
||||
def init():
|
||||
"""Create a Tool for safe_multipart and add it to cherrypy.tools."""
|
||||
cherrypy.tools.safe_multipart = cherrypy.Tool('before_request_body',
|
||||
safe_multipart)
|
||||
|
||||
705
cherrypy/lib/sessions.py
Normal file
@@ -0,0 +1,705 @@
|
||||
"""Session implementation for CherryPy.
|
||||
|
||||
We use cherrypy.request to store some convenient variables as
|
||||
well as data about the session for the current request. Instead of
|
||||
polluting cherrypy.request we use a Session object bound to
|
||||
cherrypy.session to store these variables.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
import random
|
||||
try:
|
||||
# Python 2.5+
|
||||
from hashlib import sha1 as sha
|
||||
except ImportError:
|
||||
from sha import new as sha
|
||||
import time
|
||||
import threading
|
||||
import types
|
||||
from warnings import warn
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import http
|
||||
|
||||
|
||||
missing = object()
|
||||
|
||||
class Session(object):
|
||||
"""A CherryPy dict-like Session object (one per request)."""
|
||||
|
||||
__metaclass__ = cherrypy._AttributeDocstrings
|
||||
|
||||
_id = None
|
||||
id_observers = None
|
||||
id_observers__doc = "A list of callbacks to which to pass new id's."
|
||||
|
||||
id__doc = "The current session ID."
|
||||
def _get_id(self):
|
||||
return self._id
|
||||
def _set_id(self, value):
|
||||
self._id = value
|
||||
for o in self.id_observers:
|
||||
o(value)
|
||||
id = property(_get_id, _set_id, doc=id__doc)
|
||||
|
||||
timeout = 60
|
||||
timeout__doc = "Number of minutes after which to delete session data."
|
||||
|
||||
locked = False
|
||||
locked__doc = """
|
||||
If True, this session instance has exclusive read/write access
|
||||
to session data."""
|
||||
|
||||
loaded = False
|
||||
loaded__doc = """
|
||||
If True, data has been retrieved from storage. This should happen
|
||||
automatically on the first attempt to access session data."""
|
||||
|
||||
clean_thread = None
|
||||
clean_thread__doc = "Class-level Monitor which calls self.clean_up."
|
||||
|
||||
clean_freq = 5
|
||||
clean_freq__doc = "The poll rate for expired session cleanup in minutes."
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
self.id_observers = []
|
||||
self._data = {}
|
||||
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
if id is None:
|
||||
self.regenerate()
|
||||
else:
|
||||
self.id = id
|
||||
if not self._exists():
|
||||
# Expired or malicious session. Make a new one.
|
||||
# See http://www.cherrypy.org/ticket/709.
|
||||
self.id = None
|
||||
self.regenerate()
|
||||
|
||||
def regenerate(self):
|
||||
"""Replace the current session (with a new id)."""
|
||||
if self.id is not None:
|
||||
self.delete()
|
||||
|
||||
old_session_was_locked = self.locked
|
||||
if old_session_was_locked:
|
||||
self.release_lock()
|
||||
|
||||
self.id = None
|
||||
while self.id is None:
|
||||
self.id = self.generate_id()
|
||||
# Assert that the generated id is not already stored.
|
||||
if self._exists():
|
||||
self.id = None
|
||||
|
||||
if old_session_was_locked:
|
||||
self.acquire_lock()
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
pass
|
||||
|
||||
try:
|
||||
os.urandom(20)
|
||||
except (AttributeError, NotImplementedError):
|
||||
# os.urandom not available until Python 2.4. Fall back to random.random.
|
||||
def generate_id(self):
|
||||
"""Return a new session id."""
|
||||
return sha('%s' % random.random()).hexdigest()
|
||||
else:
|
||||
def generate_id(self):
|
||||
"""Return a new session id."""
|
||||
return os.urandom(20).encode('hex')
|
||||
|
||||
def save(self):
|
||||
"""Save session data."""
|
||||
try:
|
||||
# If session data has never been loaded then it's never been
|
||||
# accessed: no need to save it
|
||||
if self.loaded:
|
||||
t = datetime.timedelta(seconds = self.timeout * 60)
|
||||
expiration_time = datetime.datetime.now() + t
|
||||
self._save(expiration_time)
|
||||
|
||||
finally:
|
||||
if self.locked:
|
||||
# Always release the lock if the user didn't release it
|
||||
self.release_lock()
|
||||
|
||||
def load(self):
|
||||
"""Copy stored session data into this session instance."""
|
||||
data = self._load()
|
||||
# data is either None or a tuple (session_data, expiration_time)
|
||||
if data is None or data[1] < datetime.datetime.now():
|
||||
# Expired session: flush session data
|
||||
self._data = {}
|
||||
else:
|
||||
self._data = data[0]
|
||||
self.loaded = True
|
||||
|
||||
# Stick the clean_thread in the class, not the instance.
|
||||
# The instances are created and destroyed per-request.
|
||||
cls = self.__class__
|
||||
if self.clean_freq and not cls.clean_thread:
|
||||
# clean_up is in instancemethod and not a classmethod,
|
||||
# so that tool config can be accessed inside the method.
|
||||
t = cherrypy.process.plugins.Monitor(
|
||||
cherrypy.engine, self.clean_up, self.clean_freq * 60)
|
||||
t.subscribe()
|
||||
cls.clean_thread = t
|
||||
t.start()
|
||||
|
||||
def delete(self):
|
||||
"""Delete stored session data."""
|
||||
self._delete()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if not self.loaded: self.load()
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
del self._data[key]
|
||||
|
||||
def pop(self, key, default=missing):
|
||||
"""Remove the specified key and return the corresponding value.
|
||||
If key is not found, default is returned if given,
|
||||
otherwise KeyError is raised.
|
||||
"""
|
||||
if not self.loaded: self.load()
|
||||
if default is missing:
|
||||
return self._data.pop(key)
|
||||
else:
|
||||
return self._data.pop(key, default)
|
||||
|
||||
def __contains__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
return key in self._data
|
||||
|
||||
def has_key(self, key):
|
||||
"""D.has_key(k) -> True if D has a key k, else False."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.has_key(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.get(key, default)
|
||||
|
||||
def update(self, d):
|
||||
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
|
||||
if not self.loaded: self.load()
|
||||
self._data.update(d)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.setdefault(key, default)
|
||||
|
||||
def clear(self):
|
||||
"""D.clear() -> None. Remove all items from D."""
|
||||
if not self.loaded: self.load()
|
||||
self._data.clear()
|
||||
|
||||
def keys(self):
|
||||
"""D.keys() -> list of D's keys."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.keys()
|
||||
|
||||
def items(self):
|
||||
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.items()
|
||||
|
||||
def values(self):
|
||||
"""D.values() -> list of D's values."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.values()
|
||||
|
||||
|
||||
class RamSession(Session):
|
||||
|
||||
# Class-level objects. Don't rebind these!
|
||||
cache = {}
|
||||
locks = {}
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
now = datetime.datetime.now()
|
||||
for id, (data, expiration_time) in self.cache.items():
|
||||
if expiration_time < now:
|
||||
try:
|
||||
del self.cache[id]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
del self.locks[id]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _exists(self):
|
||||
return self.id in self.cache
|
||||
|
||||
def _load(self):
|
||||
return self.cache.get(self.id)
|
||||
|
||||
def _save(self, expiration_time):
|
||||
self.cache[self.id] = (self._data, expiration_time)
|
||||
|
||||
def _delete(self):
|
||||
del self.cache[self.id]
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len(self.cache)
|
||||
|
||||
|
||||
class FileSession(Session):
|
||||
"""Implementation of the File backend for sessions
|
||||
|
||||
storage_path: the folder where session data will be saved. Each session
|
||||
will be saved as pickle.dump(data, expiration_time) in its own file;
|
||||
the filename will be self.SESSION_PREFIX + self.id.
|
||||
"""
|
||||
|
||||
SESSION_PREFIX = 'session-'
|
||||
LOCK_SUFFIX = '.lock'
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
Session.__init__(self, id=id, **kwargs)
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for file-based sessions.
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(cls, k, v)
|
||||
|
||||
# Warn if any lock files exist at startup.
|
||||
lockfiles = [fname for fname in os.listdir(cls.storage_path)
|
||||
if (fname.startswith(cls.SESSION_PREFIX)
|
||||
and fname.endswith(cls.LOCK_SUFFIX))]
|
||||
if lockfiles:
|
||||
plural = ('', 's')[len(lockfiles) > 1]
|
||||
warn("%s session lockfile%s found at startup. If you are "
|
||||
"only running one process, then you may need to "
|
||||
"manually delete the lockfiles found at %r."
|
||||
% (len(lockfiles), plural, cls.storage_path))
|
||||
setup = classmethod(setup)
|
||||
|
||||
def _get_file_path(self):
|
||||
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
|
||||
if not os.path.abspath(f).startswith(self.storage_path):
|
||||
raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
|
||||
return f
|
||||
|
||||
def _exists(self):
|
||||
path = self._get_file_path()
|
||||
return os.path.exists(path)
|
||||
|
||||
def _load(self, path=None):
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
try:
|
||||
f = open(path, "rb")
|
||||
try:
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
except (IOError, EOFError):
|
||||
return None
|
||||
|
||||
def _save(self, expiration_time):
|
||||
f = open(self._get_file_path(), "wb")
|
||||
try:
|
||||
pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def _delete(self):
|
||||
try:
|
||||
os.unlink(self._get_file_path())
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def acquire_lock(self, path=None):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
path += self.LOCK_SUFFIX
|
||||
while True:
|
||||
try:
|
||||
lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
os.close(lockfd)
|
||||
break
|
||||
self.locked = True
|
||||
|
||||
def release_lock(self, path=None):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
os.unlink(path + self.LOCK_SUFFIX)
|
||||
self.locked = False
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
now = datetime.datetime.now()
|
||||
# Iterate over all session files in self.storage_path
|
||||
for fname in os.listdir(self.storage_path):
|
||||
if (fname.startswith(self.SESSION_PREFIX)
|
||||
and not fname.endswith(self.LOCK_SUFFIX)):
|
||||
# We have a session file: lock and load it and check
|
||||
# if it's expired. If it fails, nevermind.
|
||||
path = os.path.join(self.storage_path, fname)
|
||||
self.acquire_lock(path)
|
||||
try:
|
||||
contents = self._load(path)
|
||||
# _load returns None on IOError
|
||||
if contents is not None:
|
||||
data, expiration_time = contents
|
||||
if expiration_time < now:
|
||||
# Session expired: deleting it
|
||||
os.unlink(path)
|
||||
finally:
|
||||
self.release_lock(path)
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len([fname for fname in os.listdir(self.storage_path)
|
||||
if (fname.startswith(self.SESSION_PREFIX)
|
||||
and not fname.endswith(self.LOCK_SUFFIX))])
|
||||
|
||||
|
||||
class PostgresqlSession(Session):
|
||||
""" Implementation of the PostgreSQL backend for sessions. It assumes
|
||||
a table like this:
|
||||
|
||||
create table session (
|
||||
id varchar(40),
|
||||
data text,
|
||||
expiration_time timestamp
|
||||
)
|
||||
|
||||
You must provide your own get_db function.
|
||||
"""
|
||||
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
Session.__init__(self, id, **kwargs)
|
||||
self.cursor = self.db.cursor()
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for Postgres-based sessions.
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(cls, k, v)
|
||||
|
||||
self.db = self.get_db()
|
||||
setup = classmethod(setup)
|
||||
|
||||
def __del__(self):
|
||||
if self.cursor:
|
||||
self.cursor.close()
|
||||
self.db.commit()
|
||||
|
||||
def _exists(self):
|
||||
# Select session data from table
|
||||
self.cursor.execute('select data, expiration_time from session '
|
||||
'where id=%s', (self.id,))
|
||||
rows = self.cursor.fetchall()
|
||||
return bool(rows)
|
||||
|
||||
def _load(self):
|
||||
# Select session data from table
|
||||
self.cursor.execute('select data, expiration_time from session '
|
||||
'where id=%s', (self.id,))
|
||||
rows = self.cursor.fetchall()
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
pickled_data, expiration_time = rows[0]
|
||||
data = pickle.loads(pickled_data)
|
||||
return data, expiration_time
|
||||
|
||||
def _save(self, expiration_time):
|
||||
pickled_data = pickle.dumps(self._data, self.pickle_protocol)
|
||||
self.cursor.execute('update session set data = %s, '
|
||||
'expiration_time = %s where id = %s',
|
||||
(pickled_data, expiration_time, self.id))
|
||||
|
||||
def _delete(self):
|
||||
self.cursor.execute('delete from session where id=%s', (self.id,))
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
# We use the "for update" clause to lock the row
|
||||
self.locked = True
|
||||
self.cursor.execute('select id from session where id=%s for update',
|
||||
(self.id,))
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
# We just close the cursor and that will remove the lock
|
||||
# introduced by the "for update" clause
|
||||
self.cursor.close()
|
||||
self.locked = False
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
self.cursor.execute('delete from session where expiration_time < %s',
|
||||
(datetime.datetime.now(),))
|
||||
|
||||
|
||||
class MemcachedSession(Session):
|
||||
|
||||
# The most popular memcached client for Python isn't thread-safe.
|
||||
# Wrap all .get and .set operations in a single lock.
|
||||
mc_lock = threading.RLock()
|
||||
|
||||
# This is a seperate set of locks per session id.
|
||||
locks = {}
|
||||
|
||||
servers = ['127.0.0.1:11211']
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for memcached-based sessions.
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(cls, k, v)
|
||||
|
||||
import memcache
|
||||
cls.cache = memcache.Client(cls.servers)
|
||||
setup = classmethod(setup)
|
||||
|
||||
def _exists(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return bool(self.cache.get(self.id))
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _load(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return self.cache.get(self.id)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _save(self, expiration_time):
|
||||
# Send the expiration time as "Unix time" (seconds since 1/1/1970)
|
||||
td = int(time.mktime(expiration_time.timetuple()))
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
if not self.cache.set(self.id, (self._data, expiration_time), td):
|
||||
raise AssertionError("Session data for id %r not set." % self.id)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _delete(self):
|
||||
self.cache.delete(self.id)
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# Hook functions (for CherryPy tools)
|
||||
|
||||
def save():
|
||||
"""Save any changed session data."""
|
||||
|
||||
if not hasattr(cherrypy.serving, "session"):
|
||||
return
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(cherrypy.request, "_sessionsaved"):
|
||||
return
|
||||
cherrypy.request._sessionsaved = True
|
||||
|
||||
if cherrypy.response.stream:
|
||||
# If the body is being streamed, we have to save the data
|
||||
# *after* the response has been written out
|
||||
cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save)
|
||||
else:
|
||||
# If the body is not being streamed, we save the data now
|
||||
# (so we can release the lock).
|
||||
if isinstance(cherrypy.response.body, types.GeneratorType):
|
||||
cherrypy.response.collapse_body()
|
||||
cherrypy.session.save()
|
||||
save.failsafe = True
|
||||
|
||||
def close():
|
||||
"""Close the session object for this request."""
|
||||
sess = getattr(cherrypy.serving, "session", None)
|
||||
if getattr(sess, "locked", False):
|
||||
# If the session is still locked we release the lock
|
||||
sess.release_lock()
|
||||
close.failsafe = True
|
||||
close.priority = 90
|
||||
|
||||
|
||||
def init(storage_type='ram', path=None, path_header=None, name='session_id',
|
||||
timeout=60, domain=None, secure=False, clean_freq=5,
|
||||
persistent=True, **kwargs):
|
||||
"""Initialize session object (using cookies).
|
||||
|
||||
storage_type: one of 'ram', 'file', 'postgresql'. This will be used
|
||||
to look up the corresponding class in cherrypy.lib.sessions
|
||||
globals. For example, 'file' will use the FileSession class.
|
||||
path: the 'path' value to stick in the response cookie metadata.
|
||||
path_header: if 'path' is None (the default), then the response
|
||||
cookie 'path' will be pulled from request.headers[path_header].
|
||||
name: the name of the cookie.
|
||||
timeout: the expiration timeout (in minutes) for the stored session data.
|
||||
If 'persistent' is True (the default), this is also the timeout
|
||||
for the cookie.
|
||||
domain: the cookie domain.
|
||||
secure: if False (the default) the cookie 'secure' value will not
|
||||
be set. If True, the cookie 'secure' value will be set (to 1).
|
||||
clean_freq (minutes): the poll rate for expired session cleanup.
|
||||
persistent: if True (the default), the 'timeout' argument will be used
|
||||
to expire the cookie. If False, the cookie will not have an expiry,
|
||||
and the cookie will be a "session cookie" which expires when the
|
||||
browser is closed.
|
||||
|
||||
Any additional kwargs will be bound to the new Session instance,
|
||||
and may be specific to the storage type. See the subclass of Session
|
||||
you're using for more information.
|
||||
"""
|
||||
|
||||
request = cherrypy.request
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(request, "_session_init_flag"):
|
||||
return
|
||||
request._session_init_flag = True
|
||||
|
||||
# Check if request came with a session ID
|
||||
id = None
|
||||
if name in request.cookie:
|
||||
id = request.cookie[name].value
|
||||
|
||||
# Find the storage class and call setup (first time only).
|
||||
storage_class = storage_type.title() + 'Session'
|
||||
storage_class = globals()[storage_class]
|
||||
if not hasattr(cherrypy, "session"):
|
||||
if hasattr(storage_class, "setup"):
|
||||
storage_class.setup(**kwargs)
|
||||
|
||||
# Create and attach a new Session instance to cherrypy.serving.
|
||||
# It will possess a reference to (and lock, and lazily load)
|
||||
# the requested session data.
|
||||
kwargs['timeout'] = timeout
|
||||
kwargs['clean_freq'] = clean_freq
|
||||
cherrypy.serving.session = sess = storage_class(id, **kwargs)
|
||||
def update_cookie(id):
|
||||
"""Update the cookie every time the session id changes."""
|
||||
cherrypy.response.cookie[name] = id
|
||||
sess.id_observers.append(update_cookie)
|
||||
|
||||
# Create cherrypy.session which will proxy to cherrypy.serving.session
|
||||
if not hasattr(cherrypy, "session"):
|
||||
cherrypy.session = cherrypy._ThreadLocalProxy('session')
|
||||
|
||||
if persistent:
|
||||
cookie_timeout = timeout
|
||||
else:
|
||||
# See http://support.microsoft.com/kb/223799/EN-US/
|
||||
# and http://support.mozilla.com/en-US/kb/Cookies
|
||||
cookie_timeout = None
|
||||
set_response_cookie(path=path, path_header=path_header, name=name,
|
||||
timeout=cookie_timeout, domain=domain, secure=secure)
|
||||
|
||||
|
||||
def set_response_cookie(path=None, path_header=None, name='session_id',
|
||||
timeout=60, domain=None, secure=False):
|
||||
"""Set a response cookie for the client.
|
||||
|
||||
path: the 'path' value to stick in the response cookie metadata.
|
||||
path_header: if 'path' is None (the default), then the response
|
||||
cookie 'path' will be pulled from request.headers[path_header].
|
||||
name: the name of the cookie.
|
||||
timeout: the expiration timeout for the cookie. If 0 or other boolean
|
||||
False, no 'expires' param will be set, and the cookie will be a
|
||||
"session cookie" which expires when the browser is closed.
|
||||
domain: the cookie domain.
|
||||
secure: if False (the default) the cookie 'secure' value will not
|
||||
be set. If True, the cookie 'secure' value will be set (to 1).
|
||||
"""
|
||||
# Set response cookie
|
||||
cookie = cherrypy.response.cookie
|
||||
cookie[name] = cherrypy.serving.session.id
|
||||
cookie[name]['path'] = (path or cherrypy.request.headers.get(path_header)
|
||||
or '/')
|
||||
|
||||
# We'd like to use the "max-age" param as indicated in
|
||||
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
|
||||
# save it to disk and the session is lost if people close
|
||||
# the browser. So we have to use the old "expires" ... sigh ...
|
||||
## cookie[name]['max-age'] = timeout * 60
|
||||
if timeout:
|
||||
cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
|
||||
if domain is not None:
|
||||
cookie[name]['domain'] = domain
|
||||
if secure:
|
||||
cookie[name]['secure'] = 1
|
||||
|
||||
|
||||
def expire():
|
||||
"""Expire the current session cookie."""
|
||||
name = cherrypy.request.config.get('tools.sessions.name', 'session_id')
|
||||
one_year = 60 * 60 * 24 * 365
|
||||
exp = time.gmtime(time.time() - one_year)
|
||||
t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp)
|
||||
cherrypy.response.cookie[name]['expires'] = t
|
||||
|
||||
|
||||
229
cherrypy/lib/static.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import mimetypes
|
||||
mimetypes.init()
|
||||
mimetypes.types_map['.dwg']='image/x-dwg'
|
||||
mimetypes.types_map['.ico']='image/x-icon'
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import cptools, http, file_generator_limited
|
||||
|
||||
|
||||
def serve_file(path, content_type=None, disposition=None, name=None):
|
||||
"""Set status, headers, and body in order to serve the given file.
|
||||
|
||||
The Content-Type header will be set to the content_type arg, if provided.
|
||||
If not provided, the Content-Type will be guessed by the file extension
|
||||
of the 'path' argument.
|
||||
|
||||
If disposition is not None, the Content-Disposition header will be set
|
||||
to "<disposition>; filename=<name>". If name is None, it will be set
|
||||
to the basename of path. If disposition is None, no Content-Disposition
|
||||
header will be written.
|
||||
"""
|
||||
|
||||
response = cherrypy.response
|
||||
|
||||
# If path is relative, users should fix it by making path absolute.
|
||||
# That is, CherryPy should not guess where the application root is.
|
||||
# It certainly should *not* use cwd (since CP may be invoked from a
|
||||
# variety of paths). If using tools.staticdir, you can make your relative
|
||||
# paths become absolute by supplying a value for "tools.staticdir.root".
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("'%s' is not an absolute path." % path)
|
||||
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Check if path is a directory.
|
||||
if stat.S_ISDIR(st.st_mode):
|
||||
# Let the caller deal with it as they like.
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Set the Last-Modified response header, so that
|
||||
# modified-since validation code can work.
|
||||
response.headers['Last-Modified'] = http.HTTPDate(st.st_mtime)
|
||||
cptools.validate_since()
|
||||
|
||||
if content_type is None:
|
||||
# Set content-type based on filename extension
|
||||
ext = ""
|
||||
i = path.rfind('.')
|
||||
if i != -1:
|
||||
ext = path[i:].lower()
|
||||
content_type = mimetypes.types_map.get(ext, "text/plain")
|
||||
response.headers['Content-Type'] = content_type
|
||||
|
||||
if disposition is not None:
|
||||
if name is None:
|
||||
name = os.path.basename(path)
|
||||
cd = '%s; filename="%s"' % (disposition, name)
|
||||
response.headers["Content-Disposition"] = cd
|
||||
|
||||
# Set Content-Length and use an iterable (file object)
|
||||
# this way CP won't load the whole file in memory
|
||||
c_len = st.st_size
|
||||
bodyfile = open(path, 'rb')
|
||||
|
||||
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
|
||||
if cherrypy.request.protocol >= (1, 1):
|
||||
response.headers["Accept-Ranges"] = "bytes"
|
||||
r = http.get_ranges(cherrypy.request.headers.get('Range'), c_len)
|
||||
if r == []:
|
||||
response.headers['Content-Range'] = "bytes */%s" % c_len
|
||||
message = "Invalid Range (first-byte-pos greater than Content-Length)"
|
||||
raise cherrypy.HTTPError(416, message)
|
||||
|
||||
if r:
|
||||
if len(r) == 1:
|
||||
# Return a single-part response.
|
||||
start, stop = r[0]
|
||||
if stop > c_len:
|
||||
stop = c_len
|
||||
r_len = stop - start
|
||||
response.status = "206 Partial Content"
|
||||
response.headers['Content-Range'] = ("bytes %s-%s/%s" %
|
||||
(start, stop - 1, c_len))
|
||||
response.headers['Content-Length'] = r_len
|
||||
bodyfile.seek(start)
|
||||
response.body = file_generator_limited(bodyfile, r_len)
|
||||
else:
|
||||
# Return a multipart/byteranges response.
|
||||
response.status = "206 Partial Content"
|
||||
import mimetools
|
||||
boundary = mimetools.choose_boundary()
|
||||
ct = "multipart/byteranges; boundary=%s" % boundary
|
||||
response.headers['Content-Type'] = ct
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
def file_ranges():
|
||||
# Apache compatibility:
|
||||
yield "\r\n"
|
||||
|
||||
for start, stop in r:
|
||||
yield "--" + boundary
|
||||
yield "\r\nContent-type: %s" % content_type
|
||||
yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
|
||||
% (start, stop - 1, c_len))
|
||||
bodyfile.seek(start)
|
||||
for chunk in file_generator_limited(bodyfile, stop-start):
|
||||
yield chunk
|
||||
yield "\r\n"
|
||||
# Final boundary
|
||||
yield "--" + boundary + "--"
|
||||
|
||||
# Apache compatibility:
|
||||
yield "\r\n"
|
||||
response.body = file_ranges()
|
||||
return response.body
|
||||
|
||||
response.headers['Content-Length'] = c_len
|
||||
response.body = bodyfile
|
||||
return response.body
|
||||
|
||||
def serve_download(path, name=None):
|
||||
"""Serve 'path' as an application/x-download attachment."""
|
||||
# This is such a common idiom I felt it deserved its own wrapper.
|
||||
return serve_file(path, "application/x-download", "attachment", name)
|
||||
|
||||
|
||||
def _attempt(filename, content_types):
|
||||
try:
|
||||
# you can set the content types for a
|
||||
# complete directory per extension
|
||||
content_type = None
|
||||
if content_types:
|
||||
r, ext = os.path.splitext(filename)
|
||||
content_type = content_types.get(ext[1:], None)
|
||||
serve_file(filename, content_type=content_type)
|
||||
return True
|
||||
except cherrypy.NotFound:
|
||||
# If we didn't find the static file, continue handling the
|
||||
# request. We might find a dynamic handler instead.
|
||||
return False
|
||||
|
||||
def staticdir(section, dir, root="", match="", content_types=None, index=""):
|
||||
"""Serve a static resource from the given (root +) dir.
|
||||
|
||||
If 'match' is given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
If content_types is given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
|
||||
If 'index' is provided, it should be the (relative) name of a file to
|
||||
serve for directory requests. For example, if the dir argument is
|
||||
'/home/me', the Request-URI is 'myapp', and the index arg is
|
||||
'index.html', the file '/home/me/myapp/index.html' will be sought.
|
||||
"""
|
||||
if match and not re.search(match, cherrypy.request.path_info):
|
||||
return False
|
||||
|
||||
# Allow the use of '~' to refer to a user's home directory.
|
||||
dir = os.path.expanduser(dir)
|
||||
|
||||
# If dir is relative, make absolute using "root".
|
||||
if not os.path.isabs(dir):
|
||||
if not root:
|
||||
msg = "Static dir requires an absolute dir (or root)."
|
||||
raise ValueError(msg)
|
||||
dir = os.path.join(root, dir)
|
||||
|
||||
# Determine where we are in the object tree relative to 'section'
|
||||
# (where the static tool was defined).
|
||||
if section == 'global':
|
||||
section = "/"
|
||||
section = section.rstrip(r"\/")
|
||||
branch = cherrypy.request.path_info[len(section) + 1:]
|
||||
branch = urllib.unquote(branch.lstrip(r"\/"))
|
||||
|
||||
# If branch is "", filename will end in a slash
|
||||
filename = os.path.join(dir, branch)
|
||||
|
||||
# There's a chance that the branch pulled from the URL might
|
||||
# have ".." or similar uplevel attacks in it. Check that the final
|
||||
# filename is a child of dir.
|
||||
if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
|
||||
raise cherrypy.HTTPError(403) # Forbidden
|
||||
|
||||
handled = _attempt(filename, content_types)
|
||||
if not handled:
|
||||
# Check for an index file if a folder was requested.
|
||||
if index:
|
||||
handled = _attempt(os.path.join(filename, index), content_types)
|
||||
if handled:
|
||||
cherrypy.request.is_index = filename[-1] in (r"\/")
|
||||
return handled
|
||||
|
||||
def staticfile(filename, root=None, match="", content_types=None):
|
||||
"""Serve a static resource from the given (root +) filename.
|
||||
|
||||
If 'match' is given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
If content_types is given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
"""
|
||||
if match and not re.search(match, cherrypy.request.path_info):
|
||||
return False
|
||||
|
||||
# If filename is relative, make absolute using "root".
|
||||
if not os.path.isabs(filename):
|
||||
if not root:
|
||||
msg = "Static tool requires an absolute filename (got '%s')." % filename
|
||||
raise ValueError(msg)
|
||||
filename = os.path.join(root, filename)
|
||||
|
||||
return _attempt(filename, content_types)
|
||||
184
cherrypy/lib/tidy.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Functions to run cherrypy.response through Tidy or NSGML."""
|
||||
|
||||
import cgi
|
||||
import os
|
||||
import StringIO
|
||||
import traceback
|
||||
|
||||
import cherrypy
|
||||
|
||||
def tidy(temp_dir, tidy_path, strict_xml=False, errors_to_ignore=None,
|
||||
indent=False, wrap=False, warnings=True):
|
||||
"""Run cherrypy.response through Tidy.
|
||||
|
||||
If either 'indent' or 'wrap' are specified, then response.body will be
|
||||
set to the output of tidy. Otherwise, only errors (including warnings,
|
||||
if warnings is True) will change the body.
|
||||
|
||||
Note that we use the standalone Tidy tool rather than the python
|
||||
mxTidy module. This is because this module does not seem to be
|
||||
stable and it crashes on some HTML pages (which means that the
|
||||
server would also crash)
|
||||
"""
|
||||
response = cherrypy.response
|
||||
|
||||
# the tidy tool, by its very nature it's not generator friendly,
|
||||
# so we just collapse the body and work with it.
|
||||
orig_body = response.collapse_body()
|
||||
|
||||
fct = response.headers.get('Content-Type', '')
|
||||
ct = fct.split(';')[0]
|
||||
encoding = ''
|
||||
i = fct.find('charset=')
|
||||
if i != -1:
|
||||
encoding = fct[i + 8:]
|
||||
|
||||
if ct == 'text/html':
|
||||
page_file = os.path.join(temp_dir, 'page.html')
|
||||
open(page_file, 'wb').write(orig_body)
|
||||
|
||||
out_file = os.path.join(temp_dir, 'tidy.out')
|
||||
err_file = os.path.join(temp_dir, 'tidy.err')
|
||||
tidy_enc = encoding.replace('-', '')
|
||||
if tidy_enc:
|
||||
tidy_enc = '-' + tidy_enc
|
||||
|
||||
strict_xml = ("", " -xml")[bool(strict_xml)]
|
||||
|
||||
if indent:
|
||||
indent = ' -indent'
|
||||
else:
|
||||
indent = ''
|
||||
|
||||
if wrap is False:
|
||||
wrap = ''
|
||||
else:
|
||||
try:
|
||||
wrap = ' -wrap %d' % int(tidyWrap)
|
||||
except:
|
||||
wrap = ''
|
||||
|
||||
result = os.system('"%s" %s%s%s%s -f %s -o %s %s' %
|
||||
(tidy_path, tidy_enc, strict_xml, indent, wrap,
|
||||
err_file, out_file, page_file))
|
||||
use_output = bool(indent or wrap) and not result
|
||||
if use_output:
|
||||
output = open(out_file, 'rb').read()
|
||||
|
||||
new_errs = []
|
||||
for err in open(err_file, 'rb').read().splitlines():
|
||||
if (err.find('Error') != -1 or
|
||||
(warnings and err.find('Warning') != -1)):
|
||||
ignore = 0
|
||||
for err_ign in errors_to_ignore or []:
|
||||
if err.find(err_ign) != -1:
|
||||
ignore = 1
|
||||
break
|
||||
if not ignore:
|
||||
new_errs.append(err)
|
||||
|
||||
if new_errs:
|
||||
response.body = wrong_content('<br />'.join(new_errs), orig_body)
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
return
|
||||
elif strict_xml:
|
||||
# The HTML is OK, but is it valid XML?
|
||||
# Use elementtree to parse XML
|
||||
from elementtree.ElementTree import parse
|
||||
tag_list = ['nbsp', 'quot']
|
||||
for tag in tag_list:
|
||||
orig_body = orig_body.replace('&' + tag + ';', tag.upper())
|
||||
|
||||
if encoding:
|
||||
enctag = '<?xml version="1.0" encoding="%s"?>' % encoding
|
||||
orig_body = enctag + orig_body
|
||||
|
||||
f = StringIO.StringIO(orig_body)
|
||||
try:
|
||||
tree = parse(f)
|
||||
except:
|
||||
# Wrong XML
|
||||
body_file = StringIO.StringIO()
|
||||
traceback.print_exc(file = body_file)
|
||||
body_file = '<br />'.join(body_file.getvalue())
|
||||
response.body = wrong_content(body_file, orig_body, "XML")
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
return
|
||||
|
||||
if use_output:
|
||||
response.body = [output]
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
def html_space(text):
|
||||
"""Escape text, replacing space with nbsp and tab with 4 nbsp's."""
|
||||
return cgi.escape(text).replace('\t', ' ').replace(' ', ' ')
|
||||
|
||||
def html_break(text):
|
||||
"""Escape text, replacing newline with HTML br element."""
|
||||
return cgi.escape(text).replace('\n', '<br />')
|
||||
|
||||
def wrong_content(header, body, content_type="HTML"):
|
||||
output = ["Wrong %s:<br />%s<br />" % (content_type, html_break(header))]
|
||||
for i, line in enumerate(body.splitlines()):
|
||||
output.append("%03d - %s" % (i + 1, html_space(line)))
|
||||
return "<br />".join(output)
|
||||
|
||||
|
||||
def nsgmls(temp_dir, nsgmls_path, catalog_path, errors_to_ignore=None):
|
||||
response = cherrypy.response
|
||||
|
||||
# the tidy tool, by its very nature it's not generator friendly,
|
||||
# so we just collect the body and work with it.
|
||||
orig_body = response.collapse_body()
|
||||
|
||||
fct = response.headers.get('Content-Type', '')
|
||||
ct = fct.split(';')[0]
|
||||
encoding = ''
|
||||
i = fct.find('charset=')
|
||||
if i != -1:
|
||||
encoding = fct[i + 8:]
|
||||
if ct == 'text/html':
|
||||
# Remove bits of Javascript (nsgmls doesn't seem to handle
|
||||
# them correctly (for instance, if <a appears in your
|
||||
# Javascript code nsgmls complains about it)
|
||||
while True:
|
||||
i = orig_body.find('<script')
|
||||
if i == -1:
|
||||
break
|
||||
j = orig_body.find('</script>', i)
|
||||
if j == -1:
|
||||
break
|
||||
orig_body = orig_body[:i] + orig_body[j+9:]
|
||||
|
||||
page_file = os.path.join(temp_dir, 'page.html')
|
||||
open(page_file, 'wb').write(orig_body)
|
||||
|
||||
err_file = os.path.join(temp_dir, 'nsgmls.err')
|
||||
command = ('%s -c%s -f%s -s -E10 %s' %
|
||||
(nsgmls_path, catalog_path, err_file, page_file))
|
||||
command = command.replace('\\', '/')
|
||||
os.system(command)
|
||||
errs = open(err_file, 'rb').read()
|
||||
|
||||
new_errs = []
|
||||
for err in errs.splitlines():
|
||||
ignore = False
|
||||
for err_ign in errors_to_ignore or []:
|
||||
if err.find(err_ign) != -1:
|
||||
ignore = True
|
||||
break
|
||||
if not ignore:
|
||||
new_errs.append(err)
|
||||
|
||||
if new_errs:
|
||||
response.body = wrong_content('<br />'.join(new_errs), orig_body)
|
||||
if response.headers.has_key("Content-Length"):
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
77
cherrypy/lib/wsgiapp.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""A CherryPy tool for hosting a foreign WSGI application."""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
# is this sufficient for start_response?
|
||||
def start_response(status, response_headers, exc_info=None):
|
||||
cherrypy.response.status = status
|
||||
headers_dict = dict(response_headers)
|
||||
cherrypy.response.headers.update(headers_dict)
|
||||
|
||||
def make_environ():
|
||||
"""grabbed some of below from wsgiserver.py
|
||||
|
||||
for hosting WSGI apps in non-WSGI environments (yikes!)
|
||||
"""
|
||||
|
||||
request = cherrypy.request
|
||||
|
||||
# create and populate the wsgi environ
|
||||
environ = dict()
|
||||
environ["wsgi.version"] = (1,0)
|
||||
environ["wsgi.url_scheme"] = request.scheme
|
||||
environ["wsgi.input"] = request.rfile
|
||||
environ["wsgi.errors"] = sys.stderr
|
||||
environ["wsgi.multithread"] = True
|
||||
environ["wsgi.multiprocess"] = False
|
||||
environ["wsgi.run_once"] = False
|
||||
environ["REQUEST_METHOD"] = request.method
|
||||
environ["SCRIPT_NAME"] = request.script_name
|
||||
environ["PATH_INFO"] = request.path_info
|
||||
environ["QUERY_STRING"] = request.query_string
|
||||
environ["SERVER_PROTOCOL"] = request.protocol
|
||||
environ["SERVER_NAME"] = request.local.name
|
||||
environ["SERVER_PORT"] = request.local.port
|
||||
environ["REMOTE_HOST"] = request.remote.name
|
||||
environ["REMOTE_ADDR"] = request.remote.ip
|
||||
environ["REMOTE_PORT"] = request.remote.port
|
||||
# then all the http headers
|
||||
headers = request.headers
|
||||
environ["CONTENT_TYPE"] = headers.get("Content-type", "")
|
||||
environ["CONTENT_LENGTH"] = headers.get("Content-length", "")
|
||||
for (k, v) in headers.iteritems():
|
||||
envname = "HTTP_" + k.upper().replace("-","_")
|
||||
environ[envname] = v
|
||||
return environ
|
||||
|
||||
|
||||
def run(app, env=None):
|
||||
"""Run the given WSGI app and set response.body to its output."""
|
||||
warnings.warn("This module is deprecated and will be removed in "
|
||||
"Cherrypy 3.2. See http://www.cherrypy.org/ticket/700 "
|
||||
"for more information.")
|
||||
|
||||
try:
|
||||
environ = cherrypy.request.wsgi_environ.copy()
|
||||
environ['SCRIPT_NAME'] = cherrypy.request.script_name
|
||||
environ['PATH_INFO'] = cherrypy.request.path_info
|
||||
except AttributeError:
|
||||
environ = make_environ()
|
||||
|
||||
if env:
|
||||
environ.update(env)
|
||||
|
||||
# run the wsgi app and have it set response.body
|
||||
response = app(environ, start_response)
|
||||
try:
|
||||
cherrypy.response.body = [x for x in response]
|
||||
finally:
|
||||
if hasattr(response, "close"):
|
||||
response.close()
|
||||
|
||||
return True
|
||||
|
||||
49
cherrypy/lib/xmlrpc.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
def process_body():
|
||||
"""Return (params, method) from request body."""
|
||||
try:
|
||||
import xmlrpclib
|
||||
return xmlrpclib.loads(cherrypy.request.body.read())
|
||||
except Exception:
|
||||
return ('ERROR PARAMS', ), 'ERRORMETHOD'
|
||||
|
||||
|
||||
def patched_path(path):
|
||||
"""Return 'path', doctored for RPC."""
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
if path.startswith('/RPC2/'):
|
||||
# strip the first /rpc2
|
||||
path = path[5:]
|
||||
return path
|
||||
|
||||
|
||||
def _set_response(body):
|
||||
# The XML-RPC spec (http://www.xmlrpc.com/spec) says:
|
||||
# "Unless there's a lower-level error, always return 200 OK."
|
||||
# Since Python's xmlrpclib interprets a non-200 response
|
||||
# as a "Protocol Error", we'll just return 200 every time.
|
||||
response = cherrypy.response
|
||||
response.status = '200 OK'
|
||||
response.body = body
|
||||
response.headers['Content-Type'] = 'text/xml'
|
||||
response.headers['Content-Length'] = len(body)
|
||||
|
||||
|
||||
def respond(body, encoding='utf-8', allow_none=0):
|
||||
import xmlrpclib
|
||||
if not isinstance(body, xmlrpclib.Fault):
|
||||
body = (body,)
|
||||
_set_response(xmlrpclib.dumps(body, methodresponse=1,
|
||||
encoding=encoding,
|
||||
allow_none=allow_none))
|
||||
|
||||
def on_error(*args, **kwargs):
|
||||
body = str(sys.exc_info()[1])
|
||||
import xmlrpclib
|
||||
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))
|
||||
|
||||
14
cherrypy/process/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Site container for an HTTP server.
|
||||
|
||||
A Web Site Process Bus object is used to connect applications, servers,
|
||||
and frameworks with site-wide services such as daemonization, process
|
||||
reload, signal handling, drop privileges, PID file management, logging
|
||||
for all of these, and many more.
|
||||
|
||||
The 'plugins' module defines a few abstract and concrete services for
|
||||
use with the bus. Some use tool-specific channels; see the documentation
|
||||
for each class.
|
||||
"""
|
||||
|
||||
from cherrypy.process.wspbus import bus
|
||||
from cherrypy.process import plugins, servers
|
||||
529
cherrypy/process/plugins.py
Normal file
@@ -0,0 +1,529 @@
|
||||
"""Site services for use with a Web Site Process Bus."""
|
||||
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
import signal as _signal
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class SimplePlugin(object):
|
||||
"""Plugin base class which auto-subscribes methods for known channels."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def subscribe(self):
|
||||
"""Register this object as a (multi-channel) listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
# Subscribe self.start, self.exit, etc. if present.
|
||||
method = getattr(self, channel, None)
|
||||
if method is not None:
|
||||
self.bus.subscribe(channel, method)
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unregister this object as a listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
# Unsubscribe self.start, self.exit, etc. if present.
|
||||
method = getattr(self, channel, None)
|
||||
if method is not None:
|
||||
self.bus.unsubscribe(channel, method)
|
||||
|
||||
|
||||
|
||||
class SignalHandler(object):
|
||||
"""Register bus channels (and listeners) for system signals.
|
||||
|
||||
By default, instantiating this object subscribes the following signals
|
||||
and listeners:
|
||||
|
||||
TERM: bus.exit
|
||||
HUP : bus.restart
|
||||
USR1: bus.graceful
|
||||
"""
|
||||
|
||||
# Map from signal numbers to names
|
||||
signals = {}
|
||||
for k, v in vars(_signal).items():
|
||||
if k.startswith('SIG') and not k.startswith('SIG_'):
|
||||
signals[v] = k
|
||||
del k, v
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
# Set default handlers
|
||||
self.handlers = {'SIGTERM': self.bus.exit,
|
||||
'SIGHUP': self.handle_SIGHUP,
|
||||
'SIGUSR1': self.bus.graceful,
|
||||
}
|
||||
|
||||
self._previous_handlers = {}
|
||||
|
||||
def subscribe(self):
|
||||
for sig, func in self.handlers.iteritems():
|
||||
try:
|
||||
self.set_handler(sig, func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def unsubscribe(self):
|
||||
for signum, handler in self._previous_handlers.iteritems():
|
||||
signame = self.signals[signum]
|
||||
|
||||
if handler is None:
|
||||
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
|
||||
handler = _signal.SIG_DFL
|
||||
else:
|
||||
self.bus.log("Restoring %s handler %r." % (signame, handler))
|
||||
|
||||
try:
|
||||
our_handler = _signal.signal(signum, handler)
|
||||
if our_handler is None:
|
||||
self.bus.log("Restored old %s handler %r, but our "
|
||||
"handler was not registered." %
|
||||
(signame, handler), level=30)
|
||||
except ValueError:
|
||||
self.bus.log("Unable to restore %s handler %r." %
|
||||
(signame, handler), level=40, traceback=True)
|
||||
|
||||
def set_handler(self, signal, listener=None):
|
||||
"""Subscribe a handler for the given signal (number or name).
|
||||
|
||||
If the optional 'listener' argument is provided, it will be
|
||||
subscribed as a listener for the given signal's channel.
|
||||
|
||||
If the given signal name or number is not available on the current
|
||||
platform, ValueError is raised.
|
||||
"""
|
||||
if isinstance(signal, basestring):
|
||||
signum = getattr(_signal, signal, None)
|
||||
if signum is None:
|
||||
raise ValueError("No such signal: %r" % signal)
|
||||
signame = signal
|
||||
else:
|
||||
try:
|
||||
signame = self.signals[signal]
|
||||
except KeyError:
|
||||
raise ValueError("No such signal: %r" % signal)
|
||||
signum = signal
|
||||
|
||||
prev = _signal.signal(signum, self._handle_signal)
|
||||
self._previous_handlers[signum] = prev
|
||||
|
||||
if listener is not None:
|
||||
self.bus.log("Listening for %s." % signame)
|
||||
self.bus.subscribe(signame, listener)
|
||||
|
||||
def _handle_signal(self, signum=None, frame=None):
|
||||
"""Python signal handler (self.set_handler subscribes it for you)."""
|
||||
signame = self.signals[signum]
|
||||
self.bus.log("Caught signal %s." % signame)
|
||||
self.bus.publish(signame)
|
||||
|
||||
def handle_SIGHUP(self):
|
||||
if os.isatty(sys.stdin.fileno()):
|
||||
# not daemonized (may be foreground or background)
|
||||
self.bus.log("SIGHUP caught but not daemonized. Exiting.")
|
||||
self.bus.exit()
|
||||
else:
|
||||
self.bus.log("SIGHUP caught while daemonized. Restarting.")
|
||||
self.bus.restart()
|
||||
|
||||
|
||||
try:
|
||||
import pwd, grp
|
||||
except ImportError:
|
||||
pwd, grp = None, None
|
||||
|
||||
|
||||
class DropPrivileges(SimplePlugin):
|
||||
"""Drop privileges. uid/gid arguments not available on Windows.
|
||||
|
||||
Special thanks to Gavin Baker: http://antonym.org/node/100.
|
||||
"""
|
||||
|
||||
def __init__(self, bus, umask=None, uid=None, gid=None):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.finalized = False
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.umask = umask
|
||||
|
||||
def _get_uid(self):
|
||||
return self._uid
|
||||
def _set_uid(self, val):
|
||||
if val is not None:
|
||||
if pwd is None:
|
||||
self.bus.log("pwd module not available; ignoring uid.",
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, basestring):
|
||||
val = pwd.getpwnam(val)[2]
|
||||
self._uid = val
|
||||
uid = property(_get_uid, _set_uid, doc="The uid under which to run.")
|
||||
|
||||
def _get_gid(self):
|
||||
return self._gid
|
||||
def _set_gid(self, val):
|
||||
if val is not None:
|
||||
if grp is None:
|
||||
self.bus.log("grp module not available; ignoring gid.",
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, basestring):
|
||||
val = grp.getgrnam(val)[2]
|
||||
self._gid = val
|
||||
gid = property(_get_gid, _set_gid, doc="The gid under which to run.")
|
||||
|
||||
def _get_umask(self):
|
||||
return self._umask
|
||||
def _set_umask(self, val):
|
||||
if val is not None:
|
||||
try:
|
||||
os.umask
|
||||
except AttributeError:
|
||||
self.bus.log("umask function not available; ignoring umask.",
|
||||
level=30)
|
||||
val = None
|
||||
self._umask = val
|
||||
umask = property(_get_umask, _set_umask, doc="The umask under which to run.")
|
||||
|
||||
def start(self):
|
||||
# uid/gid
|
||||
def current_ids():
|
||||
"""Return the current (uid, gid) if available."""
|
||||
name, group = None, None
|
||||
if pwd:
|
||||
name = pwd.getpwuid(os.getuid())[0]
|
||||
if grp:
|
||||
group = grp.getgrgid(os.getgid())[0]
|
||||
return name, group
|
||||
|
||||
if self.finalized:
|
||||
if not (self.uid is None and self.gid is None):
|
||||
self.bus.log('Already running as uid: %r gid: %r' %
|
||||
current_ids())
|
||||
else:
|
||||
if self.uid is None and self.gid is None:
|
||||
if pwd or grp:
|
||||
self.bus.log('uid/gid not set', level=30)
|
||||
else:
|
||||
self.bus.log('Started as uid: %r gid: %r' % current_ids())
|
||||
if self.gid is not None:
|
||||
os.setgid(self.gid)
|
||||
if self.uid is not None:
|
||||
os.setuid(self.uid)
|
||||
self.bus.log('Running as uid: %r gid: %r' % current_ids())
|
||||
|
||||
# umask
|
||||
if self.finalized:
|
||||
if self.umask is not None:
|
||||
self.bus.log('umask already set to: %03o' % self.umask)
|
||||
else:
|
||||
if self.umask is None:
|
||||
self.bus.log('umask not set', level=30)
|
||||
else:
|
||||
old_umask = os.umask(self.umask)
|
||||
self.bus.log('umask old: %03o, new: %03o' %
|
||||
(old_umask, self.umask))
|
||||
|
||||
self.finalized = True
|
||||
# This is slightly higher than the priority for server.start
|
||||
# in order to facilitate the most common use: starting on a low
|
||||
# port (which requires root) and then dropping to another user.
|
||||
start.priority = 77
|
||||
|
||||
|
||||
class Daemonizer(SimplePlugin):
|
||||
"""Daemonize the running script.
|
||||
|
||||
Use this with a Web Site Process Bus via:
|
||||
|
||||
Daemonizer(bus).subscribe()
|
||||
|
||||
When this component finishes, the process is completely decoupled from
|
||||
the parent environment. Please note that when this component is used,
|
||||
the return code from the parent process will still be 0 if a startup
|
||||
error occurs in the forked children. Errors in the initial daemonizing
|
||||
process still return proper exit codes. Therefore, if you use this
|
||||
plugin to daemonize, don't use the return code as an accurate indicator
|
||||
of whether the process fully started. In fact, that return code only
|
||||
indicates if the process succesfully finished the first fork.
|
||||
"""
|
||||
|
||||
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
|
||||
stderr='/dev/null'):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
if self.finalized:
|
||||
self.bus.log('Already deamonized.')
|
||||
|
||||
# forking has issues with threads:
|
||||
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
|
||||
# "The general problem with making fork() work in a multi-threaded
|
||||
# world is what to do with all of the threads..."
|
||||
# So we check for active threads:
|
||||
if threading.activeCount() != 1:
|
||||
self.bus.log('There are %r active threads. '
|
||||
'Daemonizing now may cause strange failures.' %
|
||||
threading.enumerate(), level=30)
|
||||
|
||||
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
|
||||
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
|
||||
|
||||
# Finish up with the current stdout/stderr
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Do first fork.
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# This is the child process. Continue.
|
||||
pass
|
||||
else:
|
||||
# This is the first parent. Exit, now that we've forked.
|
||||
self.bus.log('Forking once.')
|
||||
os._exit(0)
|
||||
except OSError, exc:
|
||||
# Python raises OSError rather than returning negative numbers.
|
||||
sys.exit("%s: fork #1 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
os.setsid()
|
||||
|
||||
# Do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
self.bus.log('Forking twice.')
|
||||
os._exit(0) # Exit second parent
|
||||
except OSError, exc:
|
||||
sys.exit("%s: fork #2 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
os.chdir("/")
|
||||
os.umask(0)
|
||||
|
||||
si = open(self.stdin, "r")
|
||||
so = open(self.stdout, "a+")
|
||||
se = open(self.stderr, "a+", 0)
|
||||
|
||||
# os.dup2(fd, fd2) will close fd2 if necessary,
|
||||
# so we don't explicitly close stdin/out/err.
|
||||
# See http://docs.python.org/lib/os-fd-ops.html
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
self.bus.log('Daemonized to PID: %s' % os.getpid())
|
||||
self.finalized = True
|
||||
start.priority = 65
|
||||
|
||||
|
||||
class PIDFile(SimplePlugin):
|
||||
"""Maintain a PID file via a WSPBus."""
|
||||
|
||||
def __init__(self, bus, pidfile):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.pidfile = pidfile
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
pid = os.getpid()
|
||||
if self.finalized:
|
||||
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
|
||||
else:
|
||||
open(self.pidfile, "wb").write(str(pid))
|
||||
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
|
||||
self.finalized = True
|
||||
start.priority = 70
|
||||
|
||||
def exit(self):
|
||||
try:
|
||||
os.remove(self.pidfile)
|
||||
self.bus.log('PID file removed: %r.' % self.pidfile)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class PerpetualTimer(threading._Timer):
|
||||
"""A subclass of threading._Timer whose run() method repeats."""
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.finished.wait(self.interval)
|
||||
if self.finished.isSet():
|
||||
return
|
||||
self.function(*self.args, **self.kwargs)
|
||||
|
||||
|
||||
class Monitor(SimplePlugin):
|
||||
"""WSPBus listener to periodically run a callback in its own thread.
|
||||
|
||||
bus: a Web Site Process Bus object.
|
||||
callback: the function to call at intervals.
|
||||
frequency: the time in seconds between callback runs.
|
||||
"""
|
||||
|
||||
frequency = 60
|
||||
|
||||
def __init__(self, bus, callback, frequency=60):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.callback = callback
|
||||
self.frequency = frequency
|
||||
self.thread = None
|
||||
|
||||
def start(self):
|
||||
"""Start our callback in its own perpetual timer thread."""
|
||||
if self.frequency > 0:
|
||||
threadname = self.__class__.__name__
|
||||
if self.thread is None:
|
||||
self.thread = PerpetualTimer(self.frequency, self.callback)
|
||||
self.thread.setName(threadname)
|
||||
self.thread.start()
|
||||
self.bus.log("Started monitor thread %r." % threadname)
|
||||
else:
|
||||
self.bus.log("Monitor thread %r already started." % threadname)
|
||||
start.priority = 70
|
||||
|
||||
def stop(self):
|
||||
"""Stop our callback's perpetual timer thread."""
|
||||
if self.thread is None:
|
||||
self.bus.log("No thread running for %s." % self.__class__.__name__)
|
||||
else:
|
||||
if self.thread is not threading.currentThread():
|
||||
name = self.thread.getName()
|
||||
self.thread.cancel()
|
||||
self.thread.join()
|
||||
self.bus.log("Stopped thread %r." % name)
|
||||
self.thread = None
|
||||
|
||||
def graceful(self):
|
||||
"""Stop the callback's perpetual timer thread and restart it."""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
|
||||
class Autoreloader(Monitor):
|
||||
"""Monitor which re-executes the process when files change."""
|
||||
|
||||
frequency = 1
|
||||
match = '.*'
|
||||
|
||||
def __init__(self, bus, frequency=1, match='.*'):
|
||||
self.mtimes = {}
|
||||
self.files = set()
|
||||
self.match = match
|
||||
Monitor.__init__(self, bus, self.run, frequency)
|
||||
|
||||
def start(self):
|
||||
"""Start our own perpetual timer thread for self.run."""
|
||||
if self.thread is None:
|
||||
self.mtimes = {}
|
||||
Monitor.start(self)
|
||||
start.priority = 70
|
||||
|
||||
def run(self):
|
||||
"""Reload the process if registered files have been modified."""
|
||||
sysfiles = set()
|
||||
for k, m in sys.modules.items():
|
||||
if re.match(self.match, k):
|
||||
if hasattr(m, '__loader__'):
|
||||
if hasattr(m.__loader__, 'archive'):
|
||||
k = m.__loader__.archive
|
||||
k = getattr(m, '__file__', None)
|
||||
sysfiles.add(k)
|
||||
|
||||
for filename in sysfiles | self.files:
|
||||
if filename:
|
||||
if filename.endswith('.pyc'):
|
||||
filename = filename[:-1]
|
||||
|
||||
oldtime = self.mtimes.get(filename, 0)
|
||||
if oldtime is None:
|
||||
# Module with no .py file. Skip it.
|
||||
continue
|
||||
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
# Either a module with no .py file, or it's been deleted.
|
||||
mtime = None
|
||||
|
||||
if filename not in self.mtimes:
|
||||
# If a module has no .py file, this will be None.
|
||||
self.mtimes[filename] = mtime
|
||||
else:
|
||||
if mtime is None or mtime > oldtime:
|
||||
# The file has been deleted or modified.
|
||||
self.bus.log("Restarting because %s changed." % filename)
|
||||
self.thread.cancel()
|
||||
self.bus.log("Stopped thread %r." % self.thread.getName())
|
||||
self.bus.restart()
|
||||
return
|
||||
|
||||
|
||||
class ThreadManager(SimplePlugin):
|
||||
"""Manager for HTTP request threads.
|
||||
|
||||
If you have control over thread creation and destruction, publish to
|
||||
the 'acquire_thread' and 'release_thread' channels (for each thread).
|
||||
This will register/unregister the current thread and publish to
|
||||
'start_thread' and 'stop_thread' listeners in the bus as needed.
|
||||
|
||||
If threads are created and destroyed by code you do not control
|
||||
(e.g., Apache), then, at the beginning of every HTTP request,
|
||||
publish to 'acquire_thread' only. You should not publish to
|
||||
'release_thread' in this case, since you do not know whether
|
||||
the thread will be re-used or not. The bus will call
|
||||
'stop_thread' listeners for you when it stops.
|
||||
"""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.threads = {}
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.bus.listeners.setdefault('acquire_thread', set())
|
||||
self.bus.listeners.setdefault('release_thread', set())
|
||||
|
||||
def acquire_thread(self):
|
||||
"""Run 'start_thread' listeners for the current thread.
|
||||
|
||||
If the current thread has already been seen, any 'start_thread'
|
||||
listeners will not be run again.
|
||||
"""
|
||||
thread_ident = threading._get_ident()
|
||||
if thread_ident not in self.threads:
|
||||
# We can't just use _get_ident as the thread ID
|
||||
# because some platforms reuse thread ID's.
|
||||
i = len(self.threads) + 1
|
||||
self.threads[thread_ident] = i
|
||||
self.bus.publish('start_thread', i)
|
||||
|
||||
def release_thread(self):
|
||||
"""Release the current thread and run 'stop_thread' listeners."""
|
||||
thread_ident = threading._get_ident()
|
||||
i = self.threads.pop(thread_ident, None)
|
||||
if i is not None:
|
||||
self.bus.publish('stop_thread', i)
|
||||
|
||||
def stop(self):
|
||||
"""Release all threads and run all 'stop_thread' listeners."""
|
||||
for thread_ident, i in self.threads.iteritems():
|
||||
self.bus.publish('stop_thread', i)
|
||||
self.threads.clear()
|
||||
graceful = stop
|
||||
|
||||
275
cherrypy/process/servers.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Adapt an HTTP server."""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ServerAdapter(object):
|
||||
"""Adapter for an HTTP server.
|
||||
|
||||
If you need to start more than one HTTP server (to serve on multiple
|
||||
ports, or protocols, etc.), you can manually register each one and then
|
||||
start them all with bus.start:
|
||||
|
||||
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
|
||||
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
|
||||
s1.subscribe()
|
||||
s2.subscribe()
|
||||
bus.start()
|
||||
"""
|
||||
|
||||
def __init__(self, bus, httpserver=None, bind_addr=None):
|
||||
self.bus = bus
|
||||
self.httpserver = httpserver
|
||||
self.bind_addr = bind_addr
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
def subscribe(self):
|
||||
self.bus.subscribe('start', self.start)
|
||||
self.bus.subscribe('stop', self.stop)
|
||||
|
||||
def unsubscribe(self):
|
||||
self.bus.unsubscribe('start', self.start)
|
||||
self.bus.unsubscribe('stop', self.stop)
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if self.bind_addr is None:
|
||||
on_what = "unknown interface (dynamic?)"
|
||||
elif isinstance(self.bind_addr, tuple):
|
||||
host, port = self.bind_addr
|
||||
on_what = "%s:%s" % (host, port)
|
||||
else:
|
||||
on_what = "socket file: %s" % self.bind_addr
|
||||
|
||||
if self.running:
|
||||
self.bus.log("Already serving on %s" % on_what)
|
||||
return
|
||||
|
||||
self.interrupt = None
|
||||
if not self.httpserver:
|
||||
raise ValueError("No HTTP server has been created.")
|
||||
|
||||
# Start the httpserver in a new thread.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
|
||||
import threading
|
||||
t = threading.Thread(target=self._start_http_thread)
|
||||
t.setName("HTTPServer " + t.getName())
|
||||
t.start()
|
||||
|
||||
self.wait()
|
||||
self.running = True
|
||||
self.bus.log("Serving on %s" % on_what)
|
||||
start.priority = 75
|
||||
|
||||
def _start_http_thread(self):
|
||||
"""HTTP servers MUST be running in new threads, so that the
|
||||
main thread persists to receive KeyboardInterrupt's. If an
|
||||
exception is raised in the httpserver's thread then it's
|
||||
trapped here, and the bus (and therefore our httpserver)
|
||||
are shut down.
|
||||
"""
|
||||
try:
|
||||
self.httpserver.start()
|
||||
except KeyboardInterrupt, exc:
|
||||
self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
|
||||
self.interrupt = exc
|
||||
self.bus.exit()
|
||||
except SystemExit, exc:
|
||||
self.bus.log("SystemExit raised: shutting down HTTP server")
|
||||
self.interrupt = exc
|
||||
self.bus.exit()
|
||||
raise
|
||||
except:
|
||||
import sys
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.log("Error in HTTP server: shutting down",
|
||||
traceback=True, level=40)
|
||||
self.bus.exit()
|
||||
raise
|
||||
|
||||
def wait(self):
|
||||
"""Wait until the HTTP server is ready to receive requests."""
|
||||
while not getattr(self.httpserver, "ready", False):
|
||||
if self.interrupt:
|
||||
raise self.interrupt
|
||||
time.sleep(.1)
|
||||
|
||||
# Wait for port to be occupied
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
host, port = self.bind_addr
|
||||
wait_for_occupied_port(host, port)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
if self.running:
|
||||
# stop() MUST block until the server is *truly* stopped.
|
||||
self.httpserver.stop()
|
||||
# Wait for the socket to be truly freed.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
self.running = False
|
||||
self.bus.log("HTTP Server %s shut down" % self.httpserver)
|
||||
else:
|
||||
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
|
||||
stop.priority = 25
|
||||
|
||||
def restart(self):
|
||||
"""Restart the HTTP server."""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
|
||||
class FlupFCGIServer(object):
|
||||
"""Adapter for a flup.server.fcgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get('bindAddress', None) is None:
|
||||
import socket
|
||||
if not hasattr(socket.socket, 'fromfd'):
|
||||
raise ValueError(
|
||||
'Dynamic FCGI server not available on this platform. '
|
||||
'You must use a static or external one by providing a '
|
||||
'legal bindAddress.')
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
def start(self):
|
||||
"""Start the FCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.fcgi import WSGIServer
|
||||
self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
# TODO: report this bug upstream to flup.
|
||||
# If we don't set _oldSIGs on Windows, we get:
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 108, in run
|
||||
# self._restoreSignalHandlers()
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 156, in _restoreSignalHandlers
|
||||
# for signum,handler in self._oldSIGs:
|
||||
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
|
||||
self.fcgiserver._installSignalHandlers = lambda: None
|
||||
self.fcgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.fcgiserver.run()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
# Forcibly stop the fcgi server main event loop.
|
||||
self.fcgiserver._keepGoing = False
|
||||
# Force all worker threads to die off.
|
||||
self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
|
||||
self.ready = False
|
||||
|
||||
|
||||
class FlupSCGIServer(object):
|
||||
"""Adapter for a flup.server.scgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
def start(self):
|
||||
"""Start the SCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.scgi import WSGIServer
|
||||
self.scgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
# TODO: report this bug upstream to flup.
|
||||
# If we don't set _oldSIGs on Windows, we get:
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 108, in run
|
||||
# self._restoreSignalHandlers()
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 156, in _restoreSignalHandlers
|
||||
# for signum,handler in self._oldSIGs:
|
||||
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
|
||||
self.scgiserver._installSignalHandlers = lambda: None
|
||||
self.scgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.scgiserver.run()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
self.ready = False
|
||||
# Forcibly stop the scgi server main event loop.
|
||||
self.scgiserver._keepGoing = False
|
||||
# Force all worker threads to die off.
|
||||
self.scgiserver._threadPool.maxSpare = 0
|
||||
|
||||
|
||||
def client_host(server_host):
|
||||
"""Return the host on which a client can connect to the given listener."""
|
||||
if server_host == '0.0.0.0':
|
||||
# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
|
||||
return '127.0.0.1'
|
||||
if server_host == '::':
|
||||
# :: is IN6ADDR_ANY, which should answer on localhost.
|
||||
return '::1'
|
||||
return server_host
|
||||
|
||||
def check_port(host, port, timeout=1.0):
|
||||
"""Raise an error if the given port is not free on the given host."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
host = client_host(host)
|
||||
port = int(port)
|
||||
|
||||
import socket
|
||||
|
||||
# AF_INET or AF_INET6 socket
|
||||
# Get the correct address family for our host (allows IPv6 addresses)
|
||||
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socktype, proto)
|
||||
# See http://groups.google.com/group/cherrypy-users/
|
||||
# browse_frm/thread/bbfe5eb39c904fe0
|
||||
s.settimeout(timeout)
|
||||
s.connect((host, port))
|
||||
s.close()
|
||||
raise IOError("Port %s is in use on %s; perhaps the previous "
|
||||
"httpserver did not shut down properly." %
|
||||
(repr(port), repr(host)))
|
||||
except socket.error:
|
||||
if s:
|
||||
s.close()
|
||||
|
||||
def wait_for_free_port(host, port):
|
||||
"""Wait for the specified port to become free (drop requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
|
||||
for trial in xrange(50):
|
||||
try:
|
||||
# we are expecting a free port, so reduce the timeout
|
||||
check_port(host, port, timeout=0.1)
|
||||
except IOError:
|
||||
# Give the old server thread time to free the port.
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
return
|
||||
|
||||
raise IOError("Port %r not free on %r" % (port, host))
|
||||
|
||||
def wait_for_occupied_port(host, port):
|
||||
"""Wait for the specified port to become active (receive requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
|
||||
for trial in xrange(50):
|
||||
try:
|
||||
check_port(host, port)
|
||||
except IOError:
|
||||
return
|
||||
else:
|
||||
time.sleep(.1)
|
||||
|
||||
raise IOError("Port %r not bound on %r" % (port, host))
|
||||
175
cherrypy/process/win32.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Windows service. Requires pywin32."""
|
||||
|
||||
import os
|
||||
import thread
|
||||
import win32api
|
||||
import win32con
|
||||
import win32event
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
from cherrypy.process import wspbus, plugins
|
||||
|
||||
|
||||
class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.is_set = False
|
||||
plugins.SimplePlugin.__init__(self, bus)
|
||||
|
||||
def start(self):
|
||||
if self.is_set:
|
||||
self.bus.log('Handler for console events already set.', level=40)
|
||||
return
|
||||
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
|
||||
if result == 0:
|
||||
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
|
||||
win32api.GetLastError(), level=40)
|
||||
else:
|
||||
self.bus.log('Set handler for console events.', level=40)
|
||||
self.is_set = True
|
||||
|
||||
def stop(self):
|
||||
if not self.is_set:
|
||||
self.bus.log('Handler for console events already off.', level=40)
|
||||
return
|
||||
|
||||
try:
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
|
||||
except ValueError:
|
||||
# "ValueError: The object has not been registered"
|
||||
result = 1
|
||||
|
||||
if result == 0:
|
||||
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
|
||||
win32api.GetLastError(), level=40)
|
||||
else:
|
||||
self.bus.log('Removed handler for console events.', level=40)
|
||||
self.is_set = False
|
||||
|
||||
def handle(self, event):
|
||||
"""Handle console control events (like Ctrl-C)."""
|
||||
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
|
||||
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
|
||||
win32con.CTRL_CLOSE_EVENT):
|
||||
self.bus.log('Console event %s: shutting down bus' % event)
|
||||
|
||||
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
|
||||
try:
|
||||
self.stop()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.bus.exit()
|
||||
# 'First to return True stops the calls'
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class Win32Bus(wspbus.Bus):
|
||||
"""A Web Site Process Bus implementation for Win32.
|
||||
|
||||
Instead of time.sleep, this bus blocks using native win32event objects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.events = {}
|
||||
wspbus.Bus.__init__(self)
|
||||
|
||||
def _get_state_event(self, state):
|
||||
"""Return a win32event for the given state (creating it if needed)."""
|
||||
try:
|
||||
return self.events[state]
|
||||
except KeyError:
|
||||
event = win32event.CreateEvent(None, 0, 0,
|
||||
u"WSPBus %s Event (pid=%r)" %
|
||||
(state.name, os.getpid()))
|
||||
self.events[state] = event
|
||||
return event
|
||||
|
||||
def _get_state(self):
|
||||
return self._state
|
||||
def _set_state(self, value):
|
||||
self._state = value
|
||||
event = self._get_state_event(value)
|
||||
win32event.PulseEvent(event)
|
||||
state = property(_get_state, _set_state)
|
||||
|
||||
def wait(self, state, interval=0.1):
|
||||
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
|
||||
|
||||
Since this class uses native win32event objects, the interval
|
||||
argument is ignored.
|
||||
"""
|
||||
if isinstance(state, (tuple, list)):
|
||||
# Don't wait for an event that beat us to the punch ;)
|
||||
if self.state not in state:
|
||||
events = tuple([self._get_state_event(s) for s in state])
|
||||
win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
|
||||
else:
|
||||
# Don't wait for an event that beat us to the punch ;)
|
||||
if self.state != state:
|
||||
event = self._get_state_event(state)
|
||||
win32event.WaitForSingleObject(event, win32event.INFINITE)
|
||||
|
||||
|
||||
class _ControlCodes(dict):
|
||||
"""Control codes used to "signal" a service via ControlService.
|
||||
|
||||
User-defined control codes are in the range 128-255. We generally use
|
||||
the standard Python value for the Linux signal and add 128. Example:
|
||||
|
||||
>>> signal.SIGUSR1
|
||||
10
|
||||
control_codes['graceful'] = 128 + 10
|
||||
"""
|
||||
|
||||
def key_for(self, obj):
|
||||
"""For the given value, return its corresponding key."""
|
||||
for key, val in self.iteritems():
|
||||
if val is obj:
|
||||
return key
|
||||
raise ValueError("The given object could not be found: %r" % obj)
|
||||
|
||||
control_codes = _ControlCodes({'graceful': 138})
|
||||
|
||||
|
||||
def signal_child(service, command):
|
||||
if command == 'stop':
|
||||
win32serviceutil.StopService(service)
|
||||
elif command == 'restart':
|
||||
win32serviceutil.RestartService(service)
|
||||
else:
|
||||
win32serviceutil.ControlService(service, control_codes[command])
|
||||
|
||||
|
||||
class PyWebService(win32serviceutil.ServiceFramework):
|
||||
"""Python Web Service."""
|
||||
|
||||
_svc_name_ = "Python Web Service"
|
||||
_svc_display_name_ = "Python Web Service"
|
||||
_svc_deps_ = None # sequence of service names on which this depends
|
||||
_exe_name_ = "pywebsvc"
|
||||
_exe_args_ = None # Default to no arguments
|
||||
|
||||
# Only exists on Windows 2000 or later, ignored on windows NT
|
||||
_svc_description_ = "Python Web Service"
|
||||
|
||||
def SvcDoRun(self):
|
||||
from cherrypy import process
|
||||
process.bus.start()
|
||||
process.bus.block()
|
||||
|
||||
def SvcStop(self):
|
||||
from cherrypy import process
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
process.bus.exit()
|
||||
|
||||
def SvcOther(self, control):
|
||||
process.bus.publish(control_codes.key_for(control))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
win32serviceutil.HandleCommandLine(PyWebService)
|
||||
345
cherrypy/process/wspbus.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""An implementation of the Web Site Process Bus.
|
||||
|
||||
This module is completely standalone, depending only on the stdlib.
|
||||
|
||||
Web Site Process Bus
|
||||
--------------------
|
||||
|
||||
A Bus object is used to contain and manage site-wide behavior:
|
||||
daemonization, HTTP server start/stop, process reload, signal handling,
|
||||
drop privileges, PID file management, logging for all of these,
|
||||
and many more.
|
||||
|
||||
In addition, a Bus object provides a place for each web framework
|
||||
to register code that runs in response to site-wide events (like
|
||||
process start and stop), or which controls or otherwise interacts with
|
||||
the site-wide components mentioned above. For example, a framework which
|
||||
uses file-based templates would add known template filenames to an
|
||||
autoreload component.
|
||||
|
||||
Ideally, a Bus object will be flexible enough to be useful in a variety
|
||||
of invocation scenarios:
|
||||
|
||||
1. The deployer starts a site from the command line via a framework-
|
||||
neutral deployment script; applications from multiple frameworks
|
||||
are mixed in a single site. Command-line arguments and configuration
|
||||
files are used to define site-wide components such as the HTTP server,
|
||||
WSGI component graph, autoreload behavior, signal handling, etc.
|
||||
2. The deployer starts a site via some other process, such as Apache;
|
||||
applications from multiple frameworks are mixed in a single site.
|
||||
Autoreload and signal handling (from Python at least) are disabled.
|
||||
3. The deployer starts a site via a framework-specific mechanism;
|
||||
for example, when running tests, exploring tutorials, or deploying
|
||||
single applications from a single framework. The framework controls
|
||||
which site-wide components are enabled as it sees fit.
|
||||
|
||||
The Bus object in this package uses topic-based publish-subscribe
|
||||
messaging to accomplish all this. A few topic channels are built in
|
||||
('start', 'stop', 'exit', and 'graceful'). Frameworks and site containers
|
||||
are free to define their own. If a message is sent to a channel that has
|
||||
not been defined or has no listeners, there is no effect.
|
||||
|
||||
In general, there should only ever be a single Bus object per process.
|
||||
Frameworks and site containers share a single Bus object by publishing
|
||||
messages and subscribing listeners.
|
||||
|
||||
The Bus object works as a finite state machine which models the current
|
||||
state of the process. Bus methods move it from one state to another;
|
||||
those methods then publish to subscribed listeners on the channel for
|
||||
the new state.
|
||||
|
||||
O
|
||||
|
|
||||
V
|
||||
STOPPING --> STOPPED --> EXITING -> X
|
||||
A A |
|
||||
| \___ |
|
||||
| \ |
|
||||
| V V
|
||||
STARTED <-- STARTING
|
||||
|
||||
"""
|
||||
|
||||
import atexit
|
||||
import os
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback as _traceback
|
||||
import warnings
|
||||
|
||||
|
||||
# Use a flag to indicate the state of the bus.
|
||||
class _StateEnum(object):
|
||||
class State(object):
|
||||
name = None
|
||||
def __repr__(self):
|
||||
return "states.%s" % self.name
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, self.State):
|
||||
value.name = key
|
||||
object.__setattr__(self, key, value)
|
||||
states = _StateEnum()
|
||||
states.STOPPED = states.State()
|
||||
states.STARTING = states.State()
|
||||
states.STARTED = states.State()
|
||||
states.STOPPING = states.State()
|
||||
states.EXITING = states.State()
|
||||
|
||||
|
||||
class Bus(object):
|
||||
"""Process state-machine and messenger for HTTP site deployment.
|
||||
|
||||
All listeners for a given channel are guaranteed to be called even
|
||||
if others at the same channel fail. Each failure is logged, but
|
||||
execution proceeds on to the next listener. The only way to stop all
|
||||
processing from inside a listener is to raise SystemExit and stop the
|
||||
whole server.
|
||||
"""
|
||||
|
||||
states = states
|
||||
state = states.STOPPED
|
||||
execv = False
|
||||
|
||||
def __init__(self):
|
||||
self.execv = False
|
||||
self.state = states.STOPPED
|
||||
self.listeners = dict(
|
||||
[(channel, set()) for channel
|
||||
in ('start', 'stop', 'exit', 'graceful', 'log')])
|
||||
self._priorities = {}
|
||||
|
||||
def subscribe(self, channel, callback, priority=None):
|
||||
"""Add the given callback at the given channel (if not present)."""
|
||||
if channel not in self.listeners:
|
||||
self.listeners[channel] = set()
|
||||
self.listeners[channel].add(callback)
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, 'priority', 50)
|
||||
self._priorities[(channel, callback)] = priority
|
||||
|
||||
def unsubscribe(self, channel, callback):
|
||||
"""Discard the given callback (if present)."""
|
||||
listeners = self.listeners.get(channel)
|
||||
if listeners and callback in listeners:
|
||||
listeners.discard(callback)
|
||||
del self._priorities[(channel, callback)]
|
||||
|
||||
def publish(self, channel, *args, **kwargs):
|
||||
"""Return output of all subscribers for the given channel."""
|
||||
if channel not in self.listeners:
|
||||
return []
|
||||
|
||||
exc = None
|
||||
output = []
|
||||
|
||||
items = [(self._priorities[(channel, listener)], listener)
|
||||
for listener in self.listeners[channel]]
|
||||
items.sort()
|
||||
for priority, listener in items:
|
||||
try:
|
||||
output.append(listener(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except SystemExit, e:
|
||||
# If we have previous errors ensure the exit code is non-zero
|
||||
if exc and e.code == 0:
|
||||
e.code = 1
|
||||
raise
|
||||
except:
|
||||
exc = sys.exc_info()[1]
|
||||
if channel == 'log':
|
||||
# Assume any further messages to 'log' will fail.
|
||||
pass
|
||||
else:
|
||||
self.log("Error in %r listener %r" % (channel, listener),
|
||||
level=40, traceback=True)
|
||||
if exc:
|
||||
raise
|
||||
return output
|
||||
|
||||
def _clean_exit(self):
|
||||
"""An atexit handler which asserts the Bus is not running."""
|
||||
if self.state != states.EXITING:
|
||||
warnings.warn(
|
||||
"The main thread is exiting, but the Bus is in the %r state; "
|
||||
"shutting it down automatically now. You must either call "
|
||||
"bus.block() after start(), or call bus.exit() before the "
|
||||
"main thread exits." % self.state, RuntimeWarning)
|
||||
self.exit()
|
||||
|
||||
def start(self):
|
||||
"""Start all services."""
|
||||
atexit.register(self._clean_exit)
|
||||
|
||||
self.state = states.STARTING
|
||||
self.log('Bus STARTING')
|
||||
try:
|
||||
self.publish('start')
|
||||
self.state = states.STARTED
|
||||
self.log('Bus STARTED')
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
self.log("Shutting down due to error in start listener:",
|
||||
level=40, traceback=True)
|
||||
e_info = sys.exc_info()
|
||||
try:
|
||||
self.exit()
|
||||
except:
|
||||
# Any stop/exit errors will be logged inside publish().
|
||||
pass
|
||||
raise e_info[0], e_info[1], e_info[2]
|
||||
|
||||
def exit(self):
|
||||
"""Stop all services and prepare to exit the process."""
|
||||
try:
|
||||
self.stop()
|
||||
|
||||
self.state = states.EXITING
|
||||
self.log('Bus EXITING')
|
||||
self.publish('exit')
|
||||
# This isn't strictly necessary, but it's better than seeing
|
||||
# "Waiting for child threads to terminate..." and then nothing.
|
||||
self.log('Bus EXITED')
|
||||
except:
|
||||
# This method is often called asynchronously (whether thread,
|
||||
# signal handler, console handler, or atexit handler), so we
|
||||
# can't just let exceptions propagate out unhandled.
|
||||
# Assume it's been logged and just die.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
|
||||
def restart(self):
|
||||
"""Restart the process (may close connections).
|
||||
|
||||
This method does not restart the process from the calling thread;
|
||||
instead, it stops the bus and asks the main thread to call execv.
|
||||
"""
|
||||
self.execv = True
|
||||
self.exit()
|
||||
|
||||
def graceful(self):
|
||||
"""Advise all services to reload."""
|
||||
self.log('Bus graceful')
|
||||
self.publish('graceful')
|
||||
|
||||
def block(self, interval=0.1):
|
||||
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
|
||||
|
||||
This function is intended to be called only by the main thread.
|
||||
After waiting for the EXITING state, it also waits for all threads
|
||||
to terminate, and then calls os.execv if self.execv is True. This
|
||||
design allows another thread to call bus.restart, yet have the main
|
||||
thread perform the actual execv call (required on some platforms).
|
||||
"""
|
||||
try:
|
||||
self.wait(states.EXITING, interval=interval)
|
||||
except (KeyboardInterrupt, IOError):
|
||||
# The time.sleep call might raise
|
||||
# "IOError: [Errno 4] Interrupted function call" on KBInt.
|
||||
self.log('Keyboard Interrupt: shutting down bus')
|
||||
self.exit()
|
||||
except SystemExit:
|
||||
self.log('SystemExit raised: shutting down bus')
|
||||
self.exit()
|
||||
raise
|
||||
|
||||
# Waiting for ALL child threads to finish is necessary on OS X.
|
||||
# See http://www.cherrypy.org/ticket/581.
|
||||
# It's also good to let them all shut down before allowing
|
||||
# the main thread to call atexit handlers.
|
||||
# See http://www.cherrypy.org/ticket/751.
|
||||
self.log("Waiting for child threads to terminate...")
|
||||
for t in threading.enumerate():
|
||||
if t != threading.currentThread() and t.isAlive():
|
||||
# Note that any dummy (external) threads are always daemonic.
|
||||
if hasattr(threading.Thread, "daemon"):
|
||||
# Python 2.6+
|
||||
d = t.daemon
|
||||
else:
|
||||
d = t.isDaemon()
|
||||
if not d:
|
||||
t.join()
|
||||
|
||||
if self.execv:
|
||||
self._do_execv()
|
||||
|
||||
def wait(self, state, interval=0.1):
|
||||
"""Wait for the given state(s)."""
|
||||
if isinstance(state, (tuple, list)):
|
||||
states = state
|
||||
else:
|
||||
states = [state]
|
||||
|
||||
def _wait():
|
||||
while self.state not in states:
|
||||
time.sleep(interval)
|
||||
|
||||
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
|
||||
# "The compiled machine code does not include the regular polling
|
||||
# done by Python, meaning that a KeyboardInterrupt will not be
|
||||
# detected before execution comes back to the regular Python
|
||||
# interpreter. Your program cannot be interrupted if caught
|
||||
# into an infinite Psyco-compiled loop."
|
||||
try:
|
||||
sys.modules['psyco'].cannotcompile(_wait)
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
_wait()
|
||||
|
||||
def _do_execv(self):
|
||||
"""Re-execute the current process.
|
||||
|
||||
This must be called from the main thread, because certain platforms
|
||||
(OS X) don't allow execv to be called in a child thread very well.
|
||||
"""
|
||||
args = sys.argv[:]
|
||||
self.log('Re-spawning %s' % ' '.join(args))
|
||||
args.insert(0, sys.executable)
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
|
||||
os.execv(sys.executable, args)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all services."""
|
||||
self.state = states.STOPPING
|
||||
self.log('Bus STOPPING')
|
||||
self.publish('stop')
|
||||
self.state = states.STOPPED
|
||||
self.log('Bus STOPPED')
|
||||
|
||||
def start_with_callback(self, func, args=None, kwargs=None):
|
||||
"""Start 'func' in a new thread T, then start self (and return T)."""
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
args = (func,) + args
|
||||
|
||||
def _callback(func, *a, **kw):
|
||||
self.wait(states.STARTED)
|
||||
func(*a, **kw)
|
||||
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
|
||||
t.setName('Bus Callback ' + t.getName())
|
||||
t.start()
|
||||
|
||||
self.start()
|
||||
|
||||
return t
|
||||
|
||||
def log(self, msg="", level=20, traceback=False):
|
||||
"""Log the given message. Append the last traceback if requested."""
|
||||
if traceback:
|
||||
exc = sys.exc_info()
|
||||
msg += "\n" + "".join(_traceback.format_exception(*exc))
|
||||
self.publish('log', msg, level)
|
||||
|
||||
bus = Bus()
|
||||
1849
cherrypy/wsgiserver/__init__.py
Normal file
@@ -1,22 +0,0 @@
|
||||
##
|
||||
## Bad URL Fetch Email template for SABnzbd
|
||||
## This a Cheetah template
|
||||
## Documentation: https://sabnzbd.org/wiki/extra/email-templates
|
||||
##
|
||||
## Newlines and whitespace are significant!
|
||||
##
|
||||
## These are the email headers
|
||||
To: $to
|
||||
From: $from
|
||||
Date: $date
|
||||
Subject: SABnzbd failed to fetch an NZB
|
||||
X-priority: 5
|
||||
X-MS-priority: 5
|
||||
## After this comes the body, the empty line is required!
|
||||
|
||||
Hi,
|
||||
|
||||
SABnzbd has failed to retrieve the NZB from $url.
|
||||
The error message was: $msg
|
||||
|
||||
Bye
|
||||
@@ -1,7 +1,7 @@
|
||||
##
|
||||
## Default Email template for SABnzbd
|
||||
## This a Cheetah template
|
||||
## Documentation: https://sabnzbd.org/wiki/extra/email-templates
|
||||
## Documentation: http://sabnzbd.wikidot.com/email-templates
|
||||
##
|
||||
## Newlines and whitespace are significant!
|
||||
##
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
##
|
||||
## RSS Email template for SABnzbd
|
||||
## This a Cheetah template
|
||||
## Documentation: https://sabnzbd.org/wiki/extra/email-templates
|
||||
## Documentation: http://sabnzbd.wikidot.com/email-templates
|
||||
##
|
||||
## Newlines and whitespace are significant!
|
||||
##
|
||||
|
||||
441
gntp/__init__.py
Normal file
@@ -0,0 +1,441 @@
|
||||
import re
|
||||
import hashlib
|
||||
import time
|
||||
import platform
|
||||
|
||||
__version__ = '0.4'
|
||||
|
||||
class BaseError(Exception):
|
||||
pass
|
||||
|
||||
class ParseError(BaseError):
|
||||
def gntp_error(self):
|
||||
error = GNTPError(errorcode=500,errordesc='Error parsing the message')
|
||||
return error.encode()
|
||||
|
||||
class AuthError(BaseError):
|
||||
def gntp_error(self):
|
||||
error = GNTPError(errorcode=400,errordesc='Error with authorization')
|
||||
return error.encode()
|
||||
|
||||
class UnsupportedError(BaseError):
|
||||
def gntp_error(self):
|
||||
error = GNTPError(errorcode=500,errordesc='Currently unsupported by gntp.py')
|
||||
return error.encode()
|
||||
|
||||
class _GNTPBase(object):
|
||||
info = {
|
||||
'version':'1.0',
|
||||
'messagetype':None,
|
||||
'encryptionAlgorithmID':None
|
||||
}
|
||||
_requiredHeaders = []
|
||||
headers = {}
|
||||
resources = {}
|
||||
def add_origin_info(self):
|
||||
self.add_header('Origin-Machine-Name',platform.node())
|
||||
self.add_header('Origin-Software-Name','gntp.py')
|
||||
self.add_header('Origin-Software-Version',__version__)
|
||||
self.add_header('Origin-Platform-Name',platform.system())
|
||||
self.add_header('Origin-Platform-Version',platform.platform())
|
||||
def __str__(self):
|
||||
return self.encode()
|
||||
def _parse_info(self,data):
|
||||
'''
|
||||
Parse the first line of a GNTP message to get security and other info values
|
||||
@param data: GNTP Message
|
||||
@return: GNTP Message information in a dictionary
|
||||
'''
|
||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
||||
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)'+
|
||||
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?'+
|
||||
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n', data,re.IGNORECASE)
|
||||
|
||||
if not match:
|
||||
raise ParseError('ERROR_PARSING_INFO_LINE')
|
||||
|
||||
info = match.groupdict()
|
||||
if info['encryptionAlgorithmID'] == 'NONE':
|
||||
info['encryptionAlgorithmID'] = None
|
||||
|
||||
return info
|
||||
def set_password(self,password,encryptAlgo='MD5'):
|
||||
'''
|
||||
Set a password for a GNTP Message
|
||||
@param password: Null to clear password
|
||||
@param encryptAlgo: Supports MD5,SHA1,SHA256,SHA512
|
||||
@todo: Support other hash functions
|
||||
'''
|
||||
hash = {
|
||||
'MD5': hashlib.md5,
|
||||
'SHA1': hashlib.sha1,
|
||||
'SHA256': hashlib.sha256,
|
||||
'SHA512': hashlib.sha512,
|
||||
}
|
||||
|
||||
self.password = password
|
||||
self.encryptAlgo = encryptAlgo.upper()
|
||||
if not password:
|
||||
self.info['encryptionAlgorithmID'] = None
|
||||
self.info['keyHashAlgorithm'] = None;
|
||||
return
|
||||
if not self.encryptAlgo in hash.keys():
|
||||
raise UnsupportedError('INVALID HASH "%s"'%self.encryptAlgo)
|
||||
|
||||
hashfunction = hash.get(self.encryptAlgo)
|
||||
|
||||
password = password.encode('utf8')
|
||||
seed = time.ctime()
|
||||
salt = hashfunction(seed).hexdigest()
|
||||
saltHash = hashfunction(seed).digest()
|
||||
keyBasis = password+saltHash
|
||||
key = hashfunction(keyBasis).digest()
|
||||
keyHash = hashfunction(key).hexdigest()
|
||||
|
||||
self.info['keyHashAlgorithmID'] = self.encryptAlgo
|
||||
self.info['keyHash'] = keyHash.upper()
|
||||
self.info['salt'] = salt.upper()
|
||||
def _decode_hex(self,value):
|
||||
'''
|
||||
Helper function to decode hex string to `proper` hex string
|
||||
@param value: Value to decode
|
||||
@return: Hex string
|
||||
'''
|
||||
result = ''
|
||||
for i in range(0,len(value),2):
|
||||
tmp = int(value[i:i+2],16)
|
||||
result += chr(tmp)
|
||||
return result
|
||||
def _decode_binary(self,rawIdentifier,identifier):
|
||||
rawIdentifier += '\r\n\r\n'
|
||||
dataLength = int(identifier['Length'])
|
||||
pointerStart = self.raw.find(rawIdentifier)+len(rawIdentifier)
|
||||
pointerEnd = pointerStart + dataLength
|
||||
data = self.raw[pointerStart:pointerEnd]
|
||||
if not len(data) == dataLength:
|
||||
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s'%(dataLength,len(data)))
|
||||
return data
|
||||
def _validate_password(self,password):
|
||||
'''
|
||||
Validate GNTP Message against stored password
|
||||
'''
|
||||
self.password = password
|
||||
if password == None: raise Exception()
|
||||
keyHash = self.info.get('keyHash',None)
|
||||
if keyHash is None and self.password is None:
|
||||
return True
|
||||
if keyHash is None:
|
||||
raise AuthError('Invalid keyHash')
|
||||
if self.password is None:
|
||||
raise AuthError('Missing password')
|
||||
|
||||
password = self.password.encode('utf8')
|
||||
saltHash = self._decode_hex(self.info['salt'])
|
||||
|
||||
keyBasis = password+saltHash
|
||||
key = hashlib.md5(keyBasis).digest()
|
||||
keyHash = hashlib.md5(key).hexdigest()
|
||||
|
||||
if not keyHash.upper() == self.info['keyHash'].upper():
|
||||
raise AuthError('Invalid Hash')
|
||||
return True
|
||||
def validate(self):
|
||||
'''
|
||||
Verify required headers
|
||||
'''
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header,False):
|
||||
raise ParseError('Missing Notification Header: '+header)
|
||||
|
||||
def _format_info(self):
|
||||
'''
|
||||
Generate info line for GNTP Message
|
||||
@return: Info line string
|
||||
'''
|
||||
info = u'GNTP/%s %s'%(
|
||||
self.info.get('version'),
|
||||
self.info.get('messagetype'),
|
||||
)
|
||||
if self.info.get('encryptionAlgorithmID',None):
|
||||
info += ' %s:%s'%(
|
||||
self.info.get('encryptionAlgorithmID'),
|
||||
self.info.get('ivValue'),
|
||||
)
|
||||
else:
|
||||
info+=' NONE'
|
||||
|
||||
if self.info.get('keyHashAlgorithmID',None):
|
||||
info += ' %s:%s.%s'%(
|
||||
self.info.get('keyHashAlgorithmID'),
|
||||
self.info.get('keyHash'),
|
||||
self.info.get('salt')
|
||||
)
|
||||
|
||||
return info
|
||||
def _parse_dict(self,data):
|
||||
'''
|
||||
Helper function to parse blocks of GNTP headers into a dictionary
|
||||
@param data:
|
||||
@return: Dictionary of headers
|
||||
'''
|
||||
dict = {}
|
||||
for line in data.split('\r\n'):
|
||||
match = re.match('([\w-]+):(.+)', line)
|
||||
if not match: continue
|
||||
|
||||
key = match.group(1).strip()
|
||||
val = match.group(2).strip()
|
||||
dict[key] = val
|
||||
return dict
|
||||
def add_header(self,key,value):
|
||||
if isinstance(value, unicode):
|
||||
self.headers[key] = value
|
||||
else:
|
||||
self.headers[key] = unicode('%s'%value,'utf8','replace')
|
||||
def decode(self,data,password=None):
|
||||
'''
|
||||
Decode GNTP Message
|
||||
@param data:
|
||||
'''
|
||||
self.password = password
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
def encode(self):
|
||||
'''
|
||||
Encode a GNTP Message
|
||||
@return: GNTP Message ready to be sent
|
||||
'''
|
||||
self.validate()
|
||||
EOL = u'\r\n'
|
||||
|
||||
message = self._format_info() + EOL
|
||||
#Headers
|
||||
for k,v in self.headers.iteritems():
|
||||
message += u'%s: %s%s'%(k,v,EOL)
|
||||
|
||||
message += EOL
|
||||
return message
|
||||
class GNTPRegister(_GNTPBase):
|
||||
"""Represents a GNTP Registration Command"""
|
||||
notifications = []
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notifications-Count'
|
||||
]
|
||||
_requiredNotificationHeaders = ['Notification-Name']
|
||||
def __init__(self,data=None,password=None):
|
||||
'''
|
||||
@param data: (Optional) See decode()
|
||||
@param password: (Optional) Password to use while encoding/decoding messages
|
||||
'''
|
||||
self.info['messagetype'] = 'REGISTER'
|
||||
|
||||
if data:
|
||||
self.decode(data,password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
self.add_header('Application-Name', 'pygntp')
|
||||
self.add_header('Notifications-Count', 0)
|
||||
self.add_origin_info()
|
||||
def validate(self):
|
||||
'''
|
||||
Validate required headers and validate notification headers
|
||||
'''
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header,False):
|
||||
raise ParseError('Missing Registration Header: '+header)
|
||||
for notice in self.notifications:
|
||||
for header in self._requiredNotificationHeaders:
|
||||
if not notice.get(header,False):
|
||||
raise ParseError('Missing Notification Header: '+header)
|
||||
def decode(self,data,password):
|
||||
'''
|
||||
Decode existing GNTP Registration message
|
||||
@param data: Message to decode.
|
||||
'''
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i,part in enumerate(parts):
|
||||
if i==0: continue #Skip Header
|
||||
if part.strip()=='': continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Notification-Name',False):
|
||||
self.notifications.append(notice)
|
||||
elif notice.get('Identifier',False):
|
||||
notice['Data'] = self._decode_binary(part,notice)
|
||||
#open('register.png','wblol').write(notice['Data'])
|
||||
self.resources[ notice.get('Identifier') ] = notice
|
||||
|
||||
def add_notification(self,name,enabled=True):
|
||||
'''
|
||||
Add new Notification to Registration message
|
||||
@param name: Notification Name
|
||||
@param enabled: Default Notification to Enabled
|
||||
'''
|
||||
notice = {}
|
||||
notice['Notification-Name'] = u'%s'%name
|
||||
notice['Notification-Enabled'] = u'%s'%enabled
|
||||
|
||||
self.notifications.append(notice)
|
||||
self.add_header('Notifications-Count', len(self.notifications))
|
||||
def encode(self):
|
||||
'''
|
||||
Encode a GNTP Registration Message
|
||||
@return: GNTP Registration Message ready to be sent
|
||||
'''
|
||||
self.validate()
|
||||
EOL = u'\r\n'
|
||||
|
||||
message = self._format_info() + EOL
|
||||
#Headers
|
||||
for k,v in self.headers.iteritems():
|
||||
message += u'%s: %s%s'%(k,v,EOL)
|
||||
|
||||
#Notifications
|
||||
if len(self.notifications)>0:
|
||||
for notice in self.notifications:
|
||||
message += EOL
|
||||
for k,v in notice.iteritems():
|
||||
message += u'%s: %s%s'%(k,v,EOL)
|
||||
|
||||
message += EOL
|
||||
return message
|
||||
|
||||
class GNTPNotice(_GNTPBase):
|
||||
"""Represents a GNTP Notification Command"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notification-Name',
|
||||
'Notification-Title'
|
||||
]
|
||||
def __init__(self,data=None,app=None,name=None,title=None,password=None):
|
||||
'''
|
||||
|
||||
@param data: (Optional) See decode()
|
||||
@param app: (Optional) Set Application-Name
|
||||
@param name: (Optional) Set Notification-Name
|
||||
@param title: (Optional) Set Notification Title
|
||||
@param password: (Optional) Password to use while encoding/decoding messages
|
||||
'''
|
||||
self.info['messagetype'] = 'NOTIFY'
|
||||
|
||||
if data:
|
||||
self.decode(data,password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
if app:
|
||||
self.add_header('Application-Name', app)
|
||||
if name:
|
||||
self.add_header('Notification-Name', name)
|
||||
if title:
|
||||
self.add_header('Notification-Title', title)
|
||||
self.add_origin_info()
|
||||
def decode(self,data,password):
|
||||
'''
|
||||
Decode existing GNTP Notification message
|
||||
@param data: Message to decode.
|
||||
'''
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i,part in enumerate(parts):
|
||||
if i==0: continue #Skip Header
|
||||
if part.strip()=='': continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Identifier',False):
|
||||
notice['Data'] = self._decode_binary(part,notice)
|
||||
#open('notice.png','wblol').write(notice['Data'])
|
||||
self.resources[ notice.get('Identifier') ] = notice
|
||||
def encode(self):
|
||||
'''
|
||||
Encode a GNTP Notification Message
|
||||
@return: GNTP Notification Message ready to be sent
|
||||
'''
|
||||
self.validate()
|
||||
EOL = u'\r\n'
|
||||
|
||||
message = self._format_info() + EOL
|
||||
#Headers
|
||||
for k,v in self.headers.iteritems():
|
||||
message += u'%s: %s%s'%(k,v,EOL)
|
||||
|
||||
message += EOL
|
||||
return message
|
||||
|
||||
class GNTPSubscribe(_GNTPBase):
|
||||
"""Represents a GNTP Subscribe Command"""
|
||||
def __init__(self,data=None,password=None):
|
||||
self.info['messagetype'] = 'SUBSCRIBE'
|
||||
self._requiredHeaders = [
|
||||
'Subscriber-ID',
|
||||
'Subscriber-Name',
|
||||
]
|
||||
if data:
|
||||
self.decode(data,password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
self.add_origin_info()
|
||||
|
||||
class GNTPOK(_GNTPBase):
|
||||
"""Represents a GNTP OK Response"""
|
||||
_requiredHeaders = ['Response-Action']
|
||||
def __init__(self,data=None,action=None):
|
||||
'''
|
||||
@param data: (Optional) See _GNTPResponse.decode()
|
||||
@param action: (Optional) Set type of action the OK Response is for
|
||||
'''
|
||||
self.info['messagetype'] = '-OK'
|
||||
if data:
|
||||
self.decode(data)
|
||||
if action:
|
||||
self.add_header('Response-Action', action)
|
||||
self.add_origin_info()
|
||||
|
||||
class GNTPError(_GNTPBase):
|
||||
_requiredHeaders = ['Error-Code','Error-Description']
|
||||
def __init__(self,data=None,errorcode=None,errordesc=None):
|
||||
'''
|
||||
@param data: (Optional) See _GNTPResponse.decode()
|
||||
@param errorcode: (Optional) Error code
|
||||
@param errordesc: (Optional) Error Description
|
||||
'''
|
||||
self.info['messagetype'] = '-ERROR'
|
||||
if data:
|
||||
self.decode(data)
|
||||
if errorcode:
|
||||
self.add_header('Error-Code', errorcode)
|
||||
self.add_header('Error-Description', errordesc)
|
||||
self.add_origin_info()
|
||||
def error(self):
|
||||
return self.headers['Error-Code'],self.headers['Error-Description']
|
||||
|
||||
def parse_gntp(data,password=None):
|
||||
'''
|
||||
Attempt to parse a message as a GNTP message
|
||||
@param data: Message to be parsed
|
||||
@param password: Optional password to be used to verify the message
|
||||
'''
|
||||
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',data,re.IGNORECASE)
|
||||
if not match:
|
||||
raise ParseError('INVALID_GNTP_INFO')
|
||||
info = match.groupdict()
|
||||
if info['messagetype'] == 'REGISTER':
|
||||
return GNTPRegister(data,password=password)
|
||||
elif info['messagetype'] == 'NOTIFY':
|
||||
return GNTPNotice(data,password=password)
|
||||
elif info['messagetype'] == 'SUBSCRIBE':
|
||||
return GNTPSubscribe(data,password=password)
|
||||
elif info['messagetype'] == '-OK':
|
||||
return GNTPOK(data)
|
||||
elif info['messagetype'] == '-ERROR':
|
||||
return GNTPError(data)
|
||||
raise ParseError('INVALID_GNTP_MESSAGE')
|
||||
172
gntp/notifier.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
The gntp.notifier module is provided as a simple way to send notifications
|
||||
using GNTP
|
||||
|
||||
.. note::
|
||||
This class is intended to mostly mirror the older Python bindings such
|
||||
that you should be able to replace instances of the old bindings with
|
||||
this class.
|
||||
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
|
||||
|
||||
"""
|
||||
import gntp
|
||||
import socket
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GrowlNotifier(object):
|
||||
"""Helper class to simplfy sending Growl messages
|
||||
|
||||
:param string applicationName: Sending application name
|
||||
:param list notification: List of valid notifications
|
||||
:param list defaultNotifications: List of notifications that should be enabled
|
||||
by default
|
||||
:param string applicationIcon: Icon URL
|
||||
:param string hostname: Remote host
|
||||
:param integer port: Remote port
|
||||
"""
|
||||
applicationName = 'Python GNTP'
|
||||
notifications = []
|
||||
defaultNotifications = []
|
||||
applicationIcon = None
|
||||
passwordHash = 'MD5'
|
||||
|
||||
#GNTP Specific
|
||||
password = None
|
||||
hostname = 'localhost'
|
||||
port = 23053
|
||||
|
||||
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None):
|
||||
if applicationName:
|
||||
self.applicationName = applicationName
|
||||
assert self.applicationName, 'An application name is required.'
|
||||
|
||||
if notifications:
|
||||
self.notifications = list(notifications)
|
||||
assert self.notifications, 'A sequence of one or more notification names is required.'
|
||||
|
||||
if defaultNotifications is not None:
|
||||
self.defaultNotifications = list(defaultNotifications)
|
||||
elif not self.defaultNotifications:
|
||||
self.defaultNotifications = list(self.notifications)
|
||||
|
||||
if applicationIcon is not None:
|
||||
self.applicationIcon = self._checkIcon(applicationIcon)
|
||||
elif self.applicationIcon is not None:
|
||||
self.applicationIcon = self._checkIcon(self.applicationIcon)
|
||||
|
||||
#GNTP Specific
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
if hostname:
|
||||
self.hostname = hostname
|
||||
assert self.hostname, 'Requires valid hostname'
|
||||
|
||||
if port:
|
||||
self.port = int(port)
|
||||
assert isinstance(self.port, int), 'Requires valid port'
|
||||
|
||||
def _checkIcon(self, data):
|
||||
'''
|
||||
Check the icon to see if it's valid
|
||||
@param data:
|
||||
@todo Consider checking for a valid URL
|
||||
'''
|
||||
return data
|
||||
|
||||
def register(self):
|
||||
"""Send GNTP Registration
|
||||
|
||||
.. warning::
|
||||
Before sending notifications to Growl, you need to have
|
||||
sent a registration message at least once
|
||||
"""
|
||||
logger.info('Sending registration to %s:%s', self.hostname, self.port)
|
||||
register = gntp.GNTPRegister()
|
||||
register.add_header('Application-Name', self.applicationName)
|
||||
for notification in self.notifications:
|
||||
enabled = notification in self.defaultNotifications
|
||||
register.add_notification(notification, enabled)
|
||||
if self.applicationIcon:
|
||||
register.add_header('Application-Icon', self.applicationIcon)
|
||||
if self.password:
|
||||
register.set_password(self.password, self.passwordHash)
|
||||
response = self._send('register', register.encode())
|
||||
if isinstance(response, gntp.GNTPOK):
|
||||
return True
|
||||
logger.error('Invalid response %s', response.error())
|
||||
return response.error()
|
||||
|
||||
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None):
|
||||
"""Send a GNTP notifications
|
||||
|
||||
.. warning::
|
||||
Must have registered with growl beforehand or messages will be ignored
|
||||
|
||||
:param string noteType: One of the notification names registered earlier
|
||||
:param string title: Notification title (usually displayed on the notification)
|
||||
:param string description: The main content of the notification
|
||||
:param string icon: Icon URL path
|
||||
:param boolean sticky: Sticky notification
|
||||
:param integer priority: Message priority level from -2 to 2
|
||||
"""
|
||||
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
|
||||
assert noteType in self.notifications
|
||||
notice = gntp.GNTPNotice()
|
||||
notice.add_header('Application-Name', self.applicationName)
|
||||
notice.add_header('Notification-Name', noteType)
|
||||
notice.add_header('Notification-Title', title)
|
||||
if self.password:
|
||||
notice.set_password(self.password, self.passwordHash)
|
||||
if sticky:
|
||||
notice.add_header('Notification-Sticky', sticky)
|
||||
if priority:
|
||||
notice.add_header('Notification-Priority', priority)
|
||||
if icon:
|
||||
notice.add_header('Notification-Icon', self._checkIcon(icon))
|
||||
if description:
|
||||
notice.add_header('Notification-Text', description)
|
||||
response = self._send('notify', notice.encode())
|
||||
if isinstance(response, gntp.GNTPOK):
|
||||
return True
|
||||
logger.error('Invalid response %s', response.error())
|
||||
return response.error()
|
||||
|
||||
def subscribe(self, id, name, port):
|
||||
"""Send a Subscribe request to a remote machine"""
|
||||
sub = gntp.GNTPSubscribe()
|
||||
sub.add_header('Subscriber-ID', id)
|
||||
sub.add_header('Subscriber-Name', name)
|
||||
sub.add_header('Subscriber-Port', port)
|
||||
if self.password:
|
||||
sub.set_password(self.password, self.passwordHash)
|
||||
response = self._send('subscribe', sub.encode())
|
||||
if isinstance(response, gntp.GNTPOK):
|
||||
return True
|
||||
logger.error('Invalid response %s', response.error())
|
||||
return response.error()
|
||||
|
||||
def _send(self, type, data):
|
||||
"""Send the GNTP Packet"""
|
||||
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, type, data)
|
||||
#Less verbose please
|
||||
logger.debug('To : %s:%s <%s>', self.hostname, self.port, type)
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect((self.hostname, self.port))
|
||||
s.send(data.encode('utf8', 'replace'))
|
||||
try:
|
||||
s.settimeout(10)
|
||||
except:
|
||||
pass
|
||||
response = gntp.parse_gntp(s.recv(1024))
|
||||
s.close()
|
||||
|
||||
#logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
||||
#Less verbose please
|
||||
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
|
||||
|
||||
return response
|
||||
@@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 608 608" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M630.988 320.63V188.324h99.863v-35.62l110.197 101.773L730.85 356.25v-35.62h-99.86z" fill="none" stroke-width="16.62" stroke="#000" transform="matrix(0 2.665 -2.7482 0 1003.016 -1657.67)"/><path d="M121.86 23.913h363.604v266.135h97.89l-279.69 293.667L23.97 290.048h97.89V23.913z" fill="#FFB300" class="logo-arrow-outer"/><path d="M303.664 583.797L122.274 23.975h362.78l-181.39 559.822z" class="logo-arrow-inner" fill="#FFCA28"/></svg>
|
||||
|
Before Width: | Height: | Size: 594 B |
@@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 608 608" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M630.988 320.63V188.324h99.863v-35.62l110.197 101.773L730.85 356.25v-35.62h-99.86z" fill="none" stroke-width="16.62" stroke="#000" transform="matrix(0 2.665 -2.7482 0 1003.016 -1657.67)"/><path d="M121.86 23.913h363.604v266.135h97.89l-279.69 293.667L23.97 290.048h97.89V23.913z" fill="#B9B9B9" class="logo-arrow-outer"/><path d="M303.664 583.797L122.274 23.975h362.78l-181.39 559.822z" class="logo-arrow-inner" fill="#D5D5D5"/></svg>
|
||||
|
Before Width: | Height: | Size: 594 B |
@@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 608 608" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M630.988 320.63V188.324h99.863v-35.62l110.197 101.773L730.85 356.25v-35.62h-99.86z" fill="none" stroke-width="16.62" stroke="#000" transform="matrix(0 2.665 -2.7482 0 1003.016 -1657.67)"/><path d="M121.86 23.913h363.604v266.135h97.89l-279.69 293.667L23.97 290.048h97.89V23.913z" fill="#7CB342" class="logo-arrow-outer"/><path d="M303.664 583.797L122.274 23.975h362.78l-181.39 559.822z" class="logo-arrow-inner" fill="#9CCC65"/></svg>
|
||||
|
Before Width: | Height: | Size: 594 B |
BIN
icons/nzb.ico
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |