Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5858514d6c | ||
|
|
4d891c9126 | ||
|
|
db32523990 | ||
|
|
b674e3475f | ||
|
|
977c98638c | ||
|
|
27982667ab | ||
|
|
3c3e3d4675 | ||
|
|
3944f42483 | ||
|
|
6fbdfa387b | ||
|
|
fcf814a3b4 | ||
|
|
32f53503b8 | ||
|
|
17bb97a416 | ||
|
|
ba20a77744 | ||
|
|
d173b492c6 | ||
|
|
08ed2e1636 | ||
|
|
26dfb77dfc | ||
|
|
65e1c75bc2 | ||
|
|
aec261a930 |
@@ -1,74 +0,0 @@
|
||||
# Node modules and build artifacts
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
frontend/build
|
||||
frontend/.vite
|
||||
frontend/.tauri
|
||||
|
||||
# Gradle build artifacts
|
||||
.gradle
|
||||
build
|
||||
bin
|
||||
target
|
||||
out
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Java compiled files
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Test reports
|
||||
test-results
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Runtime database and config files (locked by running app)
|
||||
app/core/configs/**
|
||||
stirling/**
|
||||
stirling-pdf-DB*.mv.db
|
||||
stirling-pdf-DB*.trace.db
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
docs
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
@@ -306,10 +306,8 @@ jobs:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||
|
||||
@@ -14,14 +14,21 @@ on:
|
||||
- macos
|
||||
- linux
|
||||
pull_request:
|
||||
branches: [main, V2, V2-tauri-windows]
|
||||
branches: [main, V2, V2-tauri-windows, v2-tauri-unix2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/desktop/**'
|
||||
- 'frontend/tsconfig.desktop.json'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
push:
|
||||
branches: [main, V2]
|
||||
branches: [main, V2, V2-tauri-windows, v2-tauri-unix2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -40,19 +47,21 @@ jobs:
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
# "macos")
|
||||
# echo 'matrix={"include":[{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# ;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# Disabled Mac builds: {"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For PR/push events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
# Disabled Mac builds: {"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}
|
||||
fi
|
||||
|
||||
build:
|
||||
@@ -77,7 +86,7 @@ jobs:
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf file libfuse2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -90,7 +99,9 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
targets: ${{ (matrix.platform == 'macos-latest' || matrix.platform == 'macos-13') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -175,6 +186,40 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
# Fix for AppImage bundling in CI environment
|
||||
- name: Configure AppImage build environment (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
echo "Setting up AppImage build environment..."
|
||||
# Ensure APPIMAGE_EXTRACT_AND_RUN is set
|
||||
echo "APPIMAGE_EXTRACT_AND_RUN=1" >> $GITHUB_ENV
|
||||
echo "NO_STRIP=true" >> $GITHUB_ENV
|
||||
echo "LINUXDEPLOY_OUTPUT_VERSION=continuous" >> $GITHUB_ENV
|
||||
|
||||
# Create directories for linuxdeploy if needed
|
||||
mkdir -p ~/.local/bin
|
||||
|
||||
# Verify FUSE2 is available
|
||||
if ! command -v fusermount &> /dev/null; then
|
||||
echo "Warning: fusermount not found, AppImage may fail"
|
||||
else
|
||||
echo "FUSE2 available: $(fusermount -V)"
|
||||
fi
|
||||
|
||||
# Verify desktop file exists
|
||||
if [ -f "./frontend/src-tauri/stirling-pdf.desktop" ]; then
|
||||
echo "Desktop file found"
|
||||
cat "./frontend/src-tauri/stirling-pdf.desktop"
|
||||
else
|
||||
echo "ERROR: Desktop file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check available tools
|
||||
echo "Checking build tools..."
|
||||
which patchelf || echo "patchelf not found"
|
||||
which file || echo "file not found"
|
||||
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
@@ -223,7 +268,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
@@ -253,49 +298,46 @@ jobs:
|
||||
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
echo "Importing Apple Developer Certificate..."
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
# Create temporary keychain
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# Import certificate
|
||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# Clean up
|
||||
rm certificate.p12
|
||||
# Disabled Mac builds - Import Apple Developer Certificate
|
||||
# - name: Import Apple Developer Certificate
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# env:
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
# run: |
|
||||
# echo "Importing Apple Developer Certificate..."
|
||||
# echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
# security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security default-keychain -s build.keychain
|
||||
# security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security set-keychain-settings -t 3600 -u build.keychain
|
||||
# security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
# security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
# security find-identity -v -p codesigning build.keychain
|
||||
# - name: Verify Certificate
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# run: |
|
||||
# echo "Verifying Apple Developer Certificate..."
|
||||
# CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
# echo "Certificate Info: $CERT_INFO"
|
||||
# CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
# echo "Certificate ID: $CERT_ID"
|
||||
# echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
# echo "Certificate imported."
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
|
||||
echo "Certificate Info: $CERT_INFO"
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "Certificate ID: $CERT_ID"
|
||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
echo "macOS version: $(sw_vers -productVersion)"
|
||||
echo "Available tools:"
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
# - name: Check DMG creation dependencies (macOS only)
|
||||
# if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
# run: |
|
||||
# echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
# echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
# echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
# echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
# echo "macOS version: $(sw_vers -productVersion)"
|
||||
# echo "Available tools:"
|
||||
# ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -303,18 +345,20 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
|
||||
# Only enable Windows signing in Tauri when on main
|
||||
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
||||
SIGN: ${{ (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
||||
CI: true
|
||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||
# Additional AppImage configuration
|
||||
LINUXDEPLOY_OUTPUT_VERSION: continuous
|
||||
NO_STRIP: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
@@ -479,21 +523,6 @@ jobs:
|
||||
Write-Host "=== Summary ==="
|
||||
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
||||
|
||||
- name: Verify notarization (macOS only)
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Verifying notarization status..."
|
||||
cd ./frontend/src-tauri/target
|
||||
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
echo "Found DMG: $DMG_FILE"
|
||||
echo "Checking notarization ticket..."
|
||||
spctl -a -vvv -t install "$DMG_FILE" || echo "⚠️ Notarization check failed or not yet complete"
|
||||
stapler validate "$DMG_FILE" || echo "⚠️ No notarization ticket attached"
|
||||
else
|
||||
echo "⚠️ No DMG file found to verify"
|
||||
fi
|
||||
|
||||
- name: Rename artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -504,12 +533,15 @@ jobs:
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
# Disabled Mac builds
|
||||
# elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
# find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
# find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
else
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
mkdir -p "../../../dist/linux"
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/linux/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.rpm" -exec cp {} "../../../dist/linux/Stirling-PDF-${{ matrix.name }}.rpm" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/linux/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
fi
|
||||
|
||||
- name: Verify Windows Code Signature
|
||||
@@ -572,6 +604,19 @@ jobs:
|
||||
Write-Host "[SUCCESS] Code signature verification completed"
|
||||
}
|
||||
|
||||
- name: List files to upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=== Files in dist directory ==="
|
||||
ls -lh ./dist/ || echo "dist directory not found"
|
||||
echo ""
|
||||
echo "=== File count by type ==="
|
||||
find ./dist -type f -name "*.exe" 2>/dev/null | wc -l | xargs echo "EXE files:"
|
||||
find ./dist -type f -name "*.msi" 2>/dev/null | wc -l | xargs echo "MSI files:"
|
||||
find ./dist -type f -name "*.deb" 2>/dev/null | wc -l | xargs echo "DEB files:"
|
||||
find ./dist -type f -name "*.rpm" 2>/dev/null | wc -l | xargs echo "RPM files:"
|
||||
find ./dist -type f -name "*.AppImage" 2>/dev/null | wc -l | xargs echo "AppImage files:"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
@@ -583,7 +628,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
echo "Checking for Windows artifacts..."
|
||||
@@ -592,22 +637,23 @@ jobs:
|
||||
echo "❌ No Windows executable found"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
echo "Checking for macOS artifacts..."
|
||||
find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No macOS artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
# Disabled Mac builds
|
||||
# elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
# echo "Checking for macOS artifacts..."
|
||||
# find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
# if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
# echo "❌ No macOS artifacts found"
|
||||
# exit 1
|
||||
# fi
|
||||
else
|
||||
echo "Checking for Linux artifacts..."
|
||||
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
||||
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | head -10
|
||||
if [ $(find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Linux artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
||||
|
||||
- name: Test artifact sizes
|
||||
|
||||
-15
@@ -33,7 +33,6 @@ SwaggerDoc.json
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.gradle-home
|
||||
.lock
|
||||
|
||||
# External tool builders
|
||||
@@ -214,17 +213,3 @@ node_modules/
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
|
||||
# Development/build artifacts
|
||||
.gradle-cache/
|
||||
scripts/pdf-collection/
|
||||
**/tmp/
|
||||
*.backup
|
||||
|
||||
# Type3 development data
|
||||
docs/type3/signatures/
|
||||
|
||||
|
||||
# Type3 sample PDFs (development only)
|
||||
**/type3/samples/
|
||||
|
||||
|
||||
@@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@@ -37,8 +37,6 @@ dependencies {
|
||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Invite management controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/invite"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/invite")
|
||||
@Tag(
|
||||
name = "Invite",
|
||||
description =
|
||||
"""
|
||||
Invite-link generation and acceptance endpoints for onboarding new users.
|
||||
|
||||
Provides the ability to issue invitation tokens, send optional email invites,
|
||||
validate and accept invite links, and manage pending invitations for teams.
|
||||
|
||||
Typical use cases include:
|
||||
• Admin workflows for issuing time-limited invitations to external users
|
||||
• Self-service invite acceptance and team assignment
|
||||
• License limit enforcement when provisioning new accounts
|
||||
|
||||
Target users: administrators and automation scripts orchestrating user onboarding.
|
||||
""")
|
||||
public @interface InviteApi {}
|
||||
@@ -505,19 +505,10 @@ public class ApplicationProperties {
|
||||
public static class Ui {
|
||||
private String appNameNavbar;
|
||||
private List<String> languages;
|
||||
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||
}
|
||||
|
||||
public String getLogoStyle() {
|
||||
// Validate and return either "modern" or "classic"
|
||||
if ("modern".equalsIgnoreCase(logoStyle)) {
|
||||
return "modern";
|
||||
}
|
||||
return "classic"; // default
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class CustomPDFDocumentFactory {
|
||||
|
||||
// Memory thresholds and limits
|
||||
|
||||
public static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
||||
private static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
||||
// Files smaller than this threshold are loaded entirely in memory for better performance.
|
||||
// These files use IOUtils.createMemoryOnlyStreamCache() which keeps all document data in RAM.
|
||||
// No temp files are created for document data, reducing I/O operations but consuming more
|
||||
|
||||
@@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -38,9 +37,6 @@ public class JobExecutorService {
|
||||
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final long effectiveTimeoutMs;
|
||||
|
||||
@Autowired(required = false)
|
||||
private JobOwnershipService jobOwnershipService;
|
||||
|
||||
public JobExecutorService(
|
||||
TaskManager taskManager,
|
||||
FileStorage fileStorage,
|
||||
@@ -102,17 +98,11 @@ public class JobExecutorService {
|
||||
long customTimeoutMs,
|
||||
boolean queueable,
|
||||
int resourceWeight) {
|
||||
// Generate base UUID
|
||||
String baseJobId = UUID.randomUUID().toString();
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
|
||||
// Scope job to authenticated user if security is enabled
|
||||
String scopedJobKey = getScopedJobKey(baseJobId);
|
||||
|
||||
log.debug("Generated jobId: {} (base: {})", scopedJobKey, baseJobId);
|
||||
|
||||
// Store the scoped job ID in the request for potential use by other components
|
||||
// Store the job ID in the request for potential use by other components
|
||||
if (request != null) {
|
||||
request.setAttribute("jobId", scopedJobKey);
|
||||
request.setAttribute("jobId", jobId);
|
||||
|
||||
// Also track this job ID in the user's session for authorization purposes
|
||||
// This ensures users can only cancel their own jobs
|
||||
@@ -126,13 +116,11 @@ public class JobExecutorService {
|
||||
request.getSession().setAttribute("userJobIds", userJobIds);
|
||||
}
|
||||
|
||||
userJobIds.add(scopedJobKey);
|
||||
log.debug("Added scoped job ID {} to user session", scopedJobKey);
|
||||
userJobIds.add(jobId);
|
||||
log.debug("Added job ID {} to user session", jobId);
|
||||
}
|
||||
}
|
||||
|
||||
String jobId = scopedJobKey;
|
||||
|
||||
// Determine which timeout to use
|
||||
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
|
||||
|
||||
@@ -161,31 +149,17 @@ public class JobExecutorService {
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
// Create a specialized wrapper that updates the TaskManager
|
||||
final String capturedJobIdForQueue = jobId;
|
||||
Supplier<Object> wrappedWork =
|
||||
() -> {
|
||||
try {
|
||||
// Set jobId in ThreadLocal context for the queued job
|
||||
stirling.software.common.util.JobContext.setJobId(
|
||||
capturedJobIdForQueue);
|
||||
log.debug(
|
||||
"Set jobId {} in JobContext for queued job execution",
|
||||
capturedJobIdForQueue);
|
||||
|
||||
Object result = work.get();
|
||||
processJobResult(capturedJobIdForQueue, result);
|
||||
processJobResult(jobId, result);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Error executing queued job {}: {}",
|
||||
capturedJobIdForQueue,
|
||||
e.getMessage(),
|
||||
e);
|
||||
taskManager.setError(capturedJobIdForQueue, e.getMessage());
|
||||
"Error executing queued job {}: {}", jobId, e.getMessage(), e);
|
||||
taskManager.setError(jobId, e.getMessage());
|
||||
throw e;
|
||||
} finally {
|
||||
// Clean up ThreadLocal to avoid memory leaks
|
||||
stirling.software.common.util.JobContext.clear();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,36 +171,21 @@ public class JobExecutorService {
|
||||
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||
} else if (async) {
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
// Capture the jobId for the async thread
|
||||
final String capturedJobId = jobId;
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
try {
|
||||
log.debug(
|
||||
"Running async job {} with timeout {} ms",
|
||||
capturedJobId,
|
||||
timeoutToUse);
|
||||
|
||||
// Set jobId in ThreadLocal context for the async thread
|
||||
stirling.software.common.util.JobContext.setJobId(capturedJobId);
|
||||
log.debug(
|
||||
"Set jobId {} in JobContext for async execution",
|
||||
capturedJobId);
|
||||
"Running async job {} with timeout {} ms", jobId, timeoutToUse);
|
||||
|
||||
// Execute with timeout
|
||||
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||
processJobResult(capturedJobId, result);
|
||||
processJobResult(jobId, result);
|
||||
} catch (TimeoutException te) {
|
||||
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
|
||||
taskManager.setError(jobId, "Job timed out");
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
||||
taskManager.setError(jobId, e.getMessage());
|
||||
} finally {
|
||||
// Clean up ThreadLocal to avoid memory leaks
|
||||
stirling.software.common.util.JobContext.clear();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -235,10 +194,6 @@ public class JobExecutorService {
|
||||
try {
|
||||
log.debug("Running sync job with timeout {} ms", timeoutToUse);
|
||||
|
||||
// Make jobId available to downstream components on the worker thread
|
||||
stirling.software.common.util.JobContext.setJobId(jobId);
|
||||
log.debug("Set jobId {} in JobContext for sync execution", jobId);
|
||||
|
||||
// Execute with timeout
|
||||
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||
|
||||
@@ -258,8 +213,6 @@ public class JobExecutorService {
|
||||
// Construct a JSON error response
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "Job failed: " + e.getMessage()));
|
||||
} finally {
|
||||
stirling.software.common.util.JobContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,23 +466,8 @@ public class JobExecutorService {
|
||||
throws TimeoutException, Exception {
|
||||
// Use the same executor as other async jobs for consistency
|
||||
// This ensures all operations run on the same thread pool
|
||||
String currentJobId = stirling.software.common.util.JobContext.getJobId();
|
||||
|
||||
java.util.concurrent.CompletableFuture<T> future =
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
if (currentJobId != null) {
|
||||
stirling.software.common.util.JobContext.setJobId(currentJobId);
|
||||
}
|
||||
try {
|
||||
return supplier.get();
|
||||
} finally {
|
||||
if (currentJobId != null) {
|
||||
stirling.software.common.util.JobContext.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
executor);
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(supplier, executor);
|
||||
|
||||
try {
|
||||
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
@@ -545,18 +483,4 @@ public class JobExecutorService {
|
||||
throw new Exception("Execution was interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a scoped job key that includes user ownership when security is enabled.
|
||||
*
|
||||
* @param baseJobId the base job identifier
|
||||
* @return scoped job key, or just baseJobId if no ownership service available
|
||||
*/
|
||||
private String getScopedJobKey(String baseJobId) {
|
||||
if (jobOwnershipService != null) {
|
||||
return jobOwnershipService.createScopedJobKey(baseJobId);
|
||||
}
|
||||
// Security disabled, return unsecured job key
|
||||
return baseJobId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service interface for managing job ownership and access control. Implementations can provide
|
||||
* user-scoped job isolation when security is enabled, or no-op behavior when security is disabled.
|
||||
*/
|
||||
public interface JobOwnershipService {
|
||||
|
||||
/**
|
||||
* Get the current authenticated user's identifier.
|
||||
*
|
||||
* @return Optional containing user identifier, or empty if not authenticated
|
||||
*/
|
||||
Optional<String> getCurrentUserId();
|
||||
|
||||
/**
|
||||
* Create a scoped job key that includes user ownership when security is enabled.
|
||||
*
|
||||
* @param jobId the base job identifier
|
||||
* @return scoped job key in format "userId:jobId", or just jobId if no user authenticated
|
||||
*/
|
||||
String createScopedJobKey(String jobId);
|
||||
|
||||
/**
|
||||
* Validate that the current user has access to the given job.
|
||||
*
|
||||
* @param scopedJobKey the scoped job key to validate
|
||||
* @return true if current user owns the job or no authentication is required
|
||||
* @throws SecurityException if current user does not own the job
|
||||
*/
|
||||
boolean validateJobAccess(String scopedJobKey);
|
||||
|
||||
/**
|
||||
* Extract the base job ID from a scoped job key.
|
||||
*
|
||||
* @param scopedJobKey the scoped job key
|
||||
* @return the base job ID without user prefix
|
||||
*/
|
||||
String extractJobId(String scopedJobKey);
|
||||
}
|
||||
@@ -38,7 +38,6 @@ public class GeneralUtils {
|
||||
Set.of(
|
||||
"OCR images.json",
|
||||
"Prepare-pdfs-for-email.json",
|
||||
"Pre-publish-sanitization.json",
|
||||
"split-rotate-auto-rename.json");
|
||||
|
||||
private final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
/** Thread-local context for passing job ID across async boundaries */
|
||||
public class JobContext {
|
||||
private static final ThreadLocal<String> CURRENT_JOB_ID = new ThreadLocal<>();
|
||||
|
||||
public static void setJobId(String jobId) {
|
||||
CURRENT_JOB_ID.set(jobId);
|
||||
}
|
||||
|
||||
public static String getJobId() {
|
||||
return CURRENT_JOB_ID.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT_JOB_ID.remove();
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,6 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getOcrMyPdfSessionLimit();
|
||||
case CFF_CONVERTER -> 1;
|
||||
};
|
||||
|
||||
long timeoutMinutes =
|
||||
@@ -151,7 +150,6 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getOcrMyPdfTimeoutMinutes();
|
||||
case CFF_CONVERTER -> 5L;
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
@@ -304,8 +302,7 @@ public class ProcessExecutor {
|
||||
TESSERACT,
|
||||
QPDF,
|
||||
GHOSTSCRIPT,
|
||||
OCR_MY_PDF,
|
||||
CFF_CONVERTER
|
||||
OCR_MY_PDF
|
||||
}
|
||||
|
||||
@Setter
|
||||
|
||||
@@ -75,9 +75,6 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
||||
// enableLogin)
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
|
||||
@@ -78,23 +78,6 @@ class JobExecutorServiceTest {
|
||||
verify(request).setAttribute(eq("jobId"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeJobIdInJobContextDuringSyncExecution() throws Exception {
|
||||
// Given
|
||||
Supplier<Object> work = stirling.software.common.util.JobContext::getJobId;
|
||||
|
||||
// When
|
||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
var requestJobIdCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(request).setAttribute(eq("jobId"), requestJobIdCaptor.capture());
|
||||
assertEquals(requestJobIdCaptor.getValue(), response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunAsyncJobSuccessfully() throws Exception {
|
||||
// Given
|
||||
|
||||
@@ -12,8 +12,6 @@ import java.util.Properties;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@@ -200,14 +198,6 @@ public class SPDFApplication {
|
||||
// }
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onWebServerInitialized(WebServerInitializedEvent event) {
|
||||
int actualPort = event.getWebServer().getPort();
|
||||
serverPortStatic = String.valueOf(actualPort);
|
||||
// Log the actual runtime port for Tauri to parse
|
||||
log.info("Stirling-PDF running on port: {}", actualPort);
|
||||
}
|
||||
|
||||
private static void printStartupLogs() {
|
||||
log.info("Stirling-PDF Started.");
|
||||
String url = baseUrlStatic + ":" + getStaticPort() + contextPathStatic;
|
||||
|
||||
+6
-87
@@ -18,37 +18,11 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
@Slf4j
|
||||
public class EndpointConfiguration {
|
||||
|
||||
public enum DisableReason {
|
||||
CONFIG,
|
||||
DEPENDENCY,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public static class EndpointAvailability {
|
||||
private final boolean enabled;
|
||||
private final DisableReason reason;
|
||||
|
||||
public EndpointAvailability(boolean enabled, DisableReason reason) {
|
||||
this.enabled = enabled;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public DisableReason getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@Getter private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
private Set<String> disabledGroups = new HashSet<>();
|
||||
private Map<String, DisableReason> endpointDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, DisableReason> groupDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
@@ -61,31 +35,16 @@ public class EndpointConfiguration {
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
|
||||
private String normalizeEndpoint(String endpoint) {
|
||||
if (endpoint == null) {
|
||||
return null;
|
||||
}
|
||||
return endpoint.startsWith("/") ? endpoint.substring(1) : endpoint;
|
||||
}
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
String normalized = normalizeEndpoint(endpoint);
|
||||
endpointStatuses.put(normalized, true);
|
||||
endpointDisableReasons.remove(normalized);
|
||||
log.debug("Enabled endpoint: {}", normalized);
|
||||
endpointStatuses.put(endpoint, true);
|
||||
log.debug("Enabled endpoint: {}", endpoint);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
disableEndpoint(endpoint, DisableReason.CONFIG);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint, DisableReason reason) {
|
||||
String normalized = normalizeEndpoint(endpoint);
|
||||
if (!Boolean.FALSE.equals(endpointStatuses.get(normalized))) {
|
||||
log.debug("Disabling endpoint: {}", normalized);
|
||||
if (!Boolean.FALSE.equals(endpointStatuses.get(endpoint))) {
|
||||
log.debug("Disabling endpoint: {}", endpoint);
|
||||
}
|
||||
endpointStatuses.put(normalized, false);
|
||||
endpointDisableReasons.put(normalized, reason);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
|
||||
public boolean isEndpointEnabled(String endpoint) {
|
||||
@@ -191,10 +150,6 @@ public class EndpointConfiguration {
|
||||
}
|
||||
|
||||
public void disableGroup(String group) {
|
||||
disableGroup(group, DisableReason.CONFIG);
|
||||
}
|
||||
|
||||
public void disableGroup(String group, DisableReason reason) {
|
||||
if (disabledGroups.add(group)) {
|
||||
if (isToolGroup(group)) {
|
||||
log.debug(
|
||||
@@ -206,12 +161,11 @@ public class EndpointConfiguration {
|
||||
group);
|
||||
}
|
||||
}
|
||||
groupDisableReasons.put(group, reason);
|
||||
// Only cascade to endpoints for *functional* groups
|
||||
if (!isToolGroup(group)) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
endpoints.forEach(endpoint -> disableEndpoint(endpoint, reason));
|
||||
endpoints.forEach(this::disableEndpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,39 +174,12 @@ public class EndpointConfiguration {
|
||||
if (disabledGroups.remove(group)) {
|
||||
log.debug("Enabling group: {}", group);
|
||||
}
|
||||
groupDisableReasons.remove(group);
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
endpoints.forEach(this::enableEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public EndpointAvailability getEndpointAvailability(String endpoint) {
|
||||
boolean enabled = isEndpointEnabled(endpoint);
|
||||
DisableReason reason = enabled ? null : determineDisableReason(endpoint);
|
||||
return new EndpointAvailability(enabled, reason);
|
||||
}
|
||||
|
||||
private DisableReason determineDisableReason(String endpoint) {
|
||||
String normalized = normalizeEndpoint(endpoint);
|
||||
if (Boolean.FALSE.equals(endpointStatuses.get(normalized))) {
|
||||
return endpointDisableReasons.getOrDefault(normalized, DisableReason.CONFIG);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
|
||||
String group = entry.getKey();
|
||||
Set<String> endpoints = entry.getValue();
|
||||
if (!disabledGroups.contains(group) || endpoints == null) {
|
||||
continue;
|
||||
}
|
||||
if (endpoints.contains(normalized)) {
|
||||
return groupDisableReasons.getOrDefault(group, DisableReason.CONFIG);
|
||||
}
|
||||
}
|
||||
|
||||
return DisableReason.UNKNOWN;
|
||||
}
|
||||
|
||||
public Set<String> getDisabledGroups() {
|
||||
return new HashSet<>(disabledGroups);
|
||||
}
|
||||
@@ -334,10 +261,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||
addEndpointToGroup("Convert", "cbz-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||
addEndpointToGroup("Convert", "pdf-to-json");
|
||||
addEndpointToGroup("Convert", "json-to-pdf");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
@@ -471,10 +394,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
addEndpointToGroup("Java", "add-attachments");
|
||||
addEndpointToGroup("Java", "compress-pdf");
|
||||
addEndpointToGroup("Java", "cbz-to-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-cbz");
|
||||
addEndpointToGroup("Java", "pdf-to-json");
|
||||
addEndpointToGroup("Java", "json-to-pdf");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
|
||||
// Javascript
|
||||
@@ -12,7 +12,6 @@ import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration.DisableReason;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
@@ -25,7 +24,6 @@ public class ExternalAppDepConfig {
|
||||
private final String weasyprintPath;
|
||||
private final String unoconvPath;
|
||||
private final Map<String, List<String>> commandToGroupMapping;
|
||||
private volatile boolean dependenciesChecked = false;
|
||||
|
||||
public ExternalAppDepConfig(
|
||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||
@@ -99,7 +97,7 @@ public class ExternalAppDepConfig {
|
||||
if (affectedGroups != null) {
|
||||
for (String group : affectedGroups) {
|
||||
List<String> affectedFeatures = getAffectedFeatures(group);
|
||||
endpointConfiguration.disableGroup(group, DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup(group);
|
||||
log.warn(
|
||||
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
|
||||
command,
|
||||
@@ -112,10 +110,6 @@ public class ExternalAppDepConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDependenciesChecked() {
|
||||
return dependenciesChecked;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void checkDependencies() {
|
||||
// Check core dependencies
|
||||
@@ -133,8 +127,8 @@ public class ExternalAppDepConfig {
|
||||
if (!pythonAvailable) {
|
||||
List<String> pythonFeatures = getAffectedFeatures("Python");
|
||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||
endpointConfiguration.disableGroup("Python", DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup("OpenCV", DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup("Python");
|
||||
endpointConfiguration.disableGroup("OpenCV");
|
||||
log.warn(
|
||||
"Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}",
|
||||
String.join(", ", pythonFeatures),
|
||||
@@ -152,14 +146,14 @@ public class ExternalAppDepConfig {
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||
endpointConfiguration.disableGroup("OpenCV", DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup("OpenCV");
|
||||
log.warn(
|
||||
"OpenCV not available in Python - Disabling OpenCV features: {}",
|
||||
String.join(", ", openCVFeatures));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||
endpointConfiguration.disableGroup("OpenCV", DisableReason.DEPENDENCY);
|
||||
endpointConfiguration.disableGroup("OpenCV");
|
||||
log.warn(
|
||||
"Error checking OpenCV: {} - Disabling OpenCV features: {}",
|
||||
e.getMessage(),
|
||||
@@ -167,7 +161,5 @@ public class ExternalAppDepConfig {
|
||||
}
|
||||
}
|
||||
endpointConfiguration.logDisabledEndpointsSummary();
|
||||
dependenciesChecked = true;
|
||||
log.info("Dependency checks completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.service.WeeklyActiveUsersService;
|
||||
|
||||
/**
|
||||
* Filter to track browser IDs for Weekly Active Users (WAU) counting. Only active when security is
|
||||
* disabled (no-login mode).
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "security.enableLogin", havingValue = "false")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WAUTrackingFilter implements Filter {
|
||||
|
||||
private final WeeklyActiveUsersService wauService;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (request instanceof HttpServletRequest httpRequest) {
|
||||
// Extract browser ID from header
|
||||
String browserId = httpRequest.getHeader("X-Browser-Id");
|
||||
|
||||
if (browserId != null && !browserId.trim().isEmpty()) {
|
||||
// Record browser access
|
||||
wauService.recordBrowserAccess(browserId);
|
||||
}
|
||||
}
|
||||
|
||||
// Continue the filter chain
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -46,24 +46,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
"https://tauri.localhost")
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
.allowedHeaders(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN",
|
||||
"X-XSRF-TOKEN",
|
||||
"X-Browser-Id")
|
||||
.exposedHeaders(
|
||||
"WWW-Authenticate",
|
||||
"X-Total-Count",
|
||||
"X-Page-Number",
|
||||
"X-Page-Size",
|
||||
"Content-Disposition",
|
||||
"Content-Type")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
} else if (hasConfiguredOrigins) {
|
||||
@@ -79,53 +63,13 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.toArray(new String[0]);
|
||||
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(allowedOrigins)
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
.allowedHeaders(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN",
|
||||
"X-XSRF-TOKEN",
|
||||
"X-Browser-Id")
|
||||
.exposedHeaders(
|
||||
"WWW-Authenticate",
|
||||
"X-Total-Count",
|
||||
"X-Page-Number",
|
||||
"X-Page-Size",
|
||||
"Content-Disposition",
|
||||
"Content-Type")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
} else {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
logger.info(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
.allowedHeaders(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN",
|
||||
"X-XSRF-TOKEN",
|
||||
"X-Browser-Id")
|
||||
.exposedHeaders(
|
||||
"WWW-Authenticate",
|
||||
"X-Total-Count",
|
||||
"X-Page-Number",
|
||||
"X-Page-Size",
|
||||
"Content-Disposition",
|
||||
"Content-Type")
|
||||
.allowedOrigins(allowedOrigins)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
// If no origins are configured and not in Tauri mode, CORS is not enabled (secure by
|
||||
// default)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -29,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -48,12 +49,13 @@ public class EditTableOfContentsController {
|
||||
@AutoJobPostMapping(
|
||||
value = "/extract-bookmarks",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@ResponseBody
|
||||
public ResponseEntity<List<Map<String, Object>>> extractBookmarks(
|
||||
@RequestParam("file") MultipartFile file) throws Exception {
|
||||
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
|
||||
throws Exception {
|
||||
PDDocument document = null;
|
||||
try {
|
||||
document = pdfDocumentFactory.load(file);
|
||||
@@ -61,10 +63,10 @@ public class EditTableOfContentsController {
|
||||
|
||||
if (outline == null) {
|
||||
log.info("No outline/bookmarks found in PDF");
|
||||
return ResponseEntity.ok(new ArrayList<>());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(extractBookmarkItems(document, outline));
|
||||
return extractBookmarkItems(document, outline);
|
||||
} finally {
|
||||
if (document != null) {
|
||||
document.close();
|
||||
|
||||
+2
-56
@@ -1,7 +1,6 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -11,13 +10,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.config.EndpointConfiguration.EndpointAvailability;
|
||||
import stirling.software.SPDF.config.InitialSetup;
|
||||
import stirling.software.common.annotations.api.ConfigApi;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
@@ -27,7 +20,6 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
@ConfigApi
|
||||
@Hidden
|
||||
@Slf4j
|
||||
public class ConfigController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@@ -35,7 +27,6 @@ public class ConfigController {
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
private final ServerCertificateServiceInterface serverCertificateService;
|
||||
private final UserServiceInterface userService;
|
||||
private final stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig;
|
||||
|
||||
public ConfigController(
|
||||
ApplicationProperties applicationProperties,
|
||||
@@ -44,14 +35,12 @@ public class ConfigController {
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
ServerCertificateServiceInterface serverCertificateService,
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
UserServiceInterface userService,
|
||||
stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig) {
|
||||
UserServiceInterface userService) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.applicationContext = applicationContext;
|
||||
this.endpointConfiguration = endpointConfiguration;
|
||||
this.serverCertificateService = serverCertificateService;
|
||||
this.userService = userService;
|
||||
this.externalAppDepConfig = externalAppDepConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/app-config")
|
||||
@@ -59,9 +48,6 @@ public class ConfigController {
|
||||
Map<String, Object> configData = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Add dependency check status
|
||||
configData.put("dependenciesReady", externalAppDepConfig.isDependenciesChecked());
|
||||
|
||||
// Get AppConfig bean
|
||||
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
|
||||
|
||||
@@ -73,15 +59,9 @@ public class ConfigController {
|
||||
// Extract values from ApplicationProperties
|
||||
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||
configData.put("languages", applicationProperties.getUi().getLanguages());
|
||||
configData.put("logoStyle", applicationProperties.getUi().getLogoStyle());
|
||||
|
||||
// Security settings
|
||||
// enableLogin requires both the config flag AND proprietary features to be loaded
|
||||
// If userService is null, proprietary module isn't loaded
|
||||
// (DISABLE_ADDITIONAL_FEATURES=true or DOCKER_ENABLE_SECURITY=false)
|
||||
boolean enableLogin =
|
||||
applicationProperties.getSecurity().getEnableLogin() && userService != null;
|
||||
configData.put("enableLogin", enableLogin);
|
||||
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
|
||||
|
||||
// Mail settings - check both SMTP enabled AND invites enabled
|
||||
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
|
||||
@@ -165,25 +145,6 @@ public class ConfigController {
|
||||
// EE features not available, continue without them
|
||||
}
|
||||
|
||||
// Add version and machine info for update checking
|
||||
try {
|
||||
if (applicationContext.containsBean("appVersion")) {
|
||||
configData.put(
|
||||
"appVersion", applicationContext.getBean("appVersion", String.class));
|
||||
}
|
||||
if (applicationContext.containsBean("machineType")) {
|
||||
configData.put(
|
||||
"machineType", applicationContext.getBean("machineType", String.class));
|
||||
}
|
||||
if (applicationContext.containsBean("activeSecurity")) {
|
||||
configData.put(
|
||||
"activeSecurity",
|
||||
applicationContext.getBean("activeSecurity", Boolean.class));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Version/machine info not available
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(configData);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -211,19 +172,4 @@ public class ConfigController {
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/endpoints-availability")
|
||||
public ResponseEntity<Map<String, EndpointAvailability>> getEndpointAvailability(
|
||||
@RequestParam(name = "endpoints")
|
||||
@Size(min = 1, max = 100, message = "Must provide between 1 and 100 endpoints")
|
||||
List<@NotBlank String> endpoints) {
|
||||
Map<String, EndpointAvailability> result = new HashMap<>();
|
||||
for (String endpoint : endpoints) {
|
||||
String trimmedEndpoint = endpoint.trim();
|
||||
result.put(
|
||||
trimmedEndpoint,
|
||||
endpointConfiguration.getEndpointAvailability(trimmedEndpoint));
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -196,9 +196,9 @@ public class StampController {
|
||||
resourceDir =
|
||||
switch (alphabet) {
|
||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
|
||||
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
|
||||
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
||||
case "korean" -> "static/fonts/malgun.ttf";
|
||||
case "chinese" -> "static/fonts/SimSun.ttf";
|
||||
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
||||
case "roman" -> "static/fonts/NotoSans-Regular.ttf";
|
||||
default -> "static/fonts/NotoSans-Regular.ttf";
|
||||
|
||||
+3
-3
@@ -171,9 +171,9 @@ public class WatermarkController {
|
||||
resourceDir =
|
||||
switch (alphabet) {
|
||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
|
||||
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
|
||||
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
||||
case "korean" -> "static/fonts/malgun.ttf";
|
||||
case "chinese" -> "static/fonts/SimSun.ttf";
|
||||
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
||||
default -> "static/fonts/NotoSans-Regular.ttf";
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointInspector;
|
||||
import stirling.software.SPDF.config.StartupApplicationListener;
|
||||
import stirling.software.SPDF.service.WeeklyActiveUsersService;
|
||||
import stirling.software.common.annotations.api.InfoApi;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@@ -35,7 +34,6 @@ public class MetricsController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final EndpointInspector endpointInspector;
|
||||
private final Optional<WeeklyActiveUsersService> wauService;
|
||||
private boolean metricsEnabled;
|
||||
|
||||
@PostConstruct
|
||||
@@ -354,36 +352,6 @@ public class MetricsController {
|
||||
return ResponseEntity.ok(formatDuration(uptime));
|
||||
}
|
||||
|
||||
@GetMapping("/wau")
|
||||
@Operation(
|
||||
summary = "Weekly Active Users statistics",
|
||||
description =
|
||||
"Returns WAU (Weekly Active Users) count and total unique browsers. "
|
||||
+ "Only available when security is disabled (no-login mode). "
|
||||
+ "Tracks unique browsers via client-generated UUID in localStorage.")
|
||||
public ResponseEntity<?> getWeeklyActiveUsers() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
}
|
||||
|
||||
// Check if WAU service is available (only when security.enableLogin=false)
|
||||
if (wauService.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(
|
||||
"WAU tracking is only available when security is disabled (no-login mode)");
|
||||
}
|
||||
|
||||
WeeklyActiveUsersService service = wauService.get();
|
||||
|
||||
Map<String, Object> wauStats = new HashMap<>();
|
||||
wauStats.put("weeklyActiveUsers", service.getWeeklyActiveUsers());
|
||||
wauStats.put("totalUniqueBrowsers", service.getTotalUniqueBrowsers());
|
||||
wauStats.put("daysOnline", service.getDaysOnline());
|
||||
wauStats.put("trackingSince", service.getStartTime().toString());
|
||||
|
||||
return ResponseEntity.ok(wauStats);
|
||||
}
|
||||
|
||||
private String formatDuration(Duration duration) {
|
||||
long days = duration.toDays();
|
||||
long hours = duration.toHoursPart();
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service for tracking Weekly Active Users (WAU) in no-login mode. Uses in-memory storage with
|
||||
* automatic cleanup of old entries.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class WeeklyActiveUsersService {
|
||||
|
||||
// Map of browser ID -> last seen timestamp
|
||||
private final Map<String, Instant> activeBrowsers = new ConcurrentHashMap<>();
|
||||
|
||||
// Track total unique browsers seen (overall)
|
||||
private long totalUniqueBrowsers = 0;
|
||||
|
||||
// Application start time
|
||||
private final Instant startTime = Instant.now();
|
||||
|
||||
/**
|
||||
* Records a browser access with the current timestamp
|
||||
*
|
||||
* @param browserId Unique browser identifier from X-Browser-Id header
|
||||
*/
|
||||
public void recordBrowserAccess(String browserId) {
|
||||
if (browserId == null || browserId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isNewBrowser = !activeBrowsers.containsKey(browserId);
|
||||
activeBrowsers.put(browserId, Instant.now());
|
||||
|
||||
if (isNewBrowser) {
|
||||
totalUniqueBrowsers++;
|
||||
log.debug("New browser recorded: {} (Total: {})", browserId, totalUniqueBrowsers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of unique browsers seen in the last 7 days
|
||||
*
|
||||
* @return Weekly Active Users count
|
||||
*/
|
||||
public long getWeeklyActiveUsers() {
|
||||
cleanupOldEntries();
|
||||
return activeBrowsers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of unique browsers ever seen
|
||||
*
|
||||
* @return Total unique browsers count
|
||||
*/
|
||||
public long getTotalUniqueBrowsers() {
|
||||
return totalUniqueBrowsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of days the service has been running
|
||||
*
|
||||
* @return Days online
|
||||
*/
|
||||
public long getDaysOnline() {
|
||||
return ChronoUnit.DAYS.between(startTime, Instant.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp when tracking started
|
||||
*
|
||||
* @return Start time
|
||||
*/
|
||||
public Instant getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
/** Removes entries older than 7 days */
|
||||
private void cleanupOldEntries() {
|
||||
Instant sevenDaysAgo = Instant.now().minus(7, ChronoUnit.DAYS);
|
||||
activeBrowsers.entrySet().removeIf(entry -> entry.getValue().isBefore(sevenDaysAgo));
|
||||
}
|
||||
|
||||
/** Manual cleanup trigger (can be called by scheduled task if needed) */
|
||||
public void performCleanup() {
|
||||
int sizeBefore = activeBrowsers.size();
|
||||
cleanupOldEntries();
|
||||
int sizeAfter = activeBrowsers.size();
|
||||
|
||||
if (sizeBefore != sizeAfter) {
|
||||
log.debug("Cleaned up {} old browser entries", sizeBefore - sizeAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -25,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobOwnershipService;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
@@ -43,9 +41,6 @@ public class JobController {
|
||||
private final JobQueue jobQueue;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Autowired(required = false)
|
||||
private JobOwnershipService jobOwnershipService;
|
||||
|
||||
/**
|
||||
* Get the status of a job
|
||||
*
|
||||
@@ -55,13 +50,6 @@ public class JobController {
|
||||
@GetMapping("/job/{jobId}")
|
||||
@Operation(summary = "Get job status")
|
||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||
// Validate job ownership
|
||||
if (!validateJobAccess(jobId)) {
|
||||
log.warn("Unauthorized attempt to access job status: {}", jobId);
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("message", "You are not authorized to access this job"));
|
||||
}
|
||||
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -91,13 +79,6 @@ public class JobController {
|
||||
@GetMapping("/job/{jobId}/result")
|
||||
@Operation(summary = "Get job result")
|
||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||
// Validate job ownership
|
||||
if (!validateJobAccess(jobId)) {
|
||||
log.warn("Unauthorized attempt to access job result: {}", jobId);
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("message", "You are not authorized to access this job"));
|
||||
}
|
||||
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -163,8 +144,13 @@ public class JobController {
|
||||
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
||||
log.debug("Request to cancel job: {}", jobId);
|
||||
|
||||
// Validate job ownership
|
||||
if (!validateJobAccess(jobId)) {
|
||||
// Verify that this job belongs to the current user
|
||||
// We can use the current request's session to validate ownership
|
||||
Object sessionJobIds = request.getSession().getAttribute("userJobIds");
|
||||
if (sessionJobIds == null
|
||||
|| !(sessionJobIds instanceof java.util.Set)
|
||||
|| !((java.util.Set<?>) sessionJobIds).contains(jobId)) {
|
||||
// Either no jobs in session or jobId doesn't match user's jobs
|
||||
log.warn("Unauthorized attempt to cancel job: {}", jobId);
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("message", "You are not authorized to cancel this job"));
|
||||
@@ -224,13 +210,6 @@ public class JobController {
|
||||
@GetMapping("/job/{jobId}/result/files")
|
||||
@Operation(summary = "Get job result files")
|
||||
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||
// Validate job ownership
|
||||
if (!validateJobAccess(jobId)) {
|
||||
log.warn("Unauthorized attempt to access job files: {}", jobId);
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("message", "You are not authorized to access this job"));
|
||||
}
|
||||
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -351,26 +330,4 @@ public class JobController {
|
||||
return "attachment; filename=\"" + fileName + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the current user has access to the given job.
|
||||
*
|
||||
* @param jobId the job identifier to validate
|
||||
* @return true if user has access, false otherwise
|
||||
*/
|
||||
private boolean validateJobAccess(String jobId) {
|
||||
// If JobOwnershipService is available (security enabled), use it
|
||||
if (jobOwnershipService != null) {
|
||||
try {
|
||||
return jobOwnershipService.validateJobAccess(jobId);
|
||||
} catch (SecurityException e) {
|
||||
log.warn("Job ownership validation failed for jobId {}: {}", jobId, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Security disabled - allow all access (backwards compatibility)
|
||||
// When security is not enabled, any user can access any job by jobId
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
multipart.enabled=true
|
||||
logging.level.org.springframework=WARN
|
||||
logging.level.org.springframework.security=WARN
|
||||
logging.level.org.hibernate=WARN
|
||||
logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.springframework.security.oauth2=DEBUG
|
||||
@@ -8,9 +7,6 @@ logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.opensaml=DEBUG
|
||||
#logging.level.stirling.software.proprietary.security=DEBUG
|
||||
logging.level.com.zaxxer.hikari=WARN
|
||||
logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO
|
||||
logging.level.stirling.software.common.service.JobExecutorService=INFO
|
||||
logging.level.stirling.software.common.service.TaskManager=INFO
|
||||
spring.jpa.open-in-view=false
|
||||
server.forward-headers-strategy=NATIVE
|
||||
server.error.path=/error
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#############################################################################################################
|
||||
|
||||
security:
|
||||
enableLogin: true # set to 'true' to enable login
|
||||
enableLogin: false # set to 'true' to enable login
|
||||
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
|
||||
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
||||
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||
@@ -174,26 +174,8 @@ system:
|
||||
databaseBackup:
|
||||
cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight
|
||||
|
||||
stirling:
|
||||
pdf:
|
||||
fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font
|
||||
json:
|
||||
font-normalization:
|
||||
enabled: false # IMPORTANT: Disable to preserve ToUnicode CMaps for correct font rendering. Ghostscript strips Unicode mappings from CID fonts.
|
||||
cff-converter:
|
||||
enabled: true # Wrap CFF/Type1C fonts as OpenType-CFF for browser compatibility
|
||||
method: python # Converter method: 'python' (fontTools, recommended - wraps as OTF), 'fontforge' (legacy - converts to TTF, may hang on CID fonts)
|
||||
python-command: /opt/venv/bin/python3 # Python interpreter path
|
||||
python-script: /scripts/convert_cff_to_ttf.py # Path to font wrapping script
|
||||
fontforge-command: fontforge # Override if FontForge is installed under a different name/path
|
||||
type3:
|
||||
library:
|
||||
enabled: true # Match common Type3 fonts against the built-in library of converted programs
|
||||
index: classpath:/type3/library/index.json # Override to point at a custom index.json (supports http:, file:, classpath:)
|
||||
|
||||
ui:
|
||||
appNameNavbar: '' # name displayed on the navigation bar
|
||||
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
|
||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||
|
||||
endpoints:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-54
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "Pre-publish-sanitization",
|
||||
"pipeline": [
|
||||
{
|
||||
"operation": "/api/v1/security/sanitize-pdf",
|
||||
"parameters": {
|
||||
"removeJavaScript": true,
|
||||
"removeEmbeddedFiles": true,
|
||||
"removeXMPMetadata": true,
|
||||
"removeMetadata": true,
|
||||
"removeLinks": true,
|
||||
"removeFonts": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation": "/api/v1/misc/flatten",
|
||||
"parameters": {
|
||||
"flattenOnlyForms": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation": "/api/v1/general/remove-annotations",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"operation": "/api/v1/misc/update-metadata",
|
||||
"parameters": {
|
||||
"deleteAll": true,
|
||||
"author": "",
|
||||
"creationDate": "",
|
||||
"creator": "",
|
||||
"keywords": "",
|
||||
"modificationDate": "",
|
||||
"producer": "",
|
||||
"subject": "",
|
||||
"title": "",
|
||||
"trapped": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation": "/api/v1/misc/compress-pdf",
|
||||
"parameters": {
|
||||
"optimizeLevel": 3,
|
||||
"expectedOutputSize": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"_examples": {
|
||||
"outputDir": "{outputFolder}/{folderName}",
|
||||
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
|
||||
},
|
||||
"outputDir": "{outputFolder}",
|
||||
"outputFileName": "pre_publish_{filename}.PDF"
|
||||
}
|
||||
@@ -1,592 +0,0 @@
|
||||
[
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "1867"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "1888"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "2029"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "2069"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "2089"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "2116"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSansMono",
|
||||
"encoding": "2174"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans-Oblique",
|
||||
"encoding": "2192"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "2209"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "Cmsy10",
|
||||
"encoding": "2228"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "STIXSizeThreeSym-Regular",
|
||||
"encoding": "2233"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSansDisplay",
|
||||
"encoding": "2239"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4403"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4438"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4519"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4685"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4733"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4782"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4813"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4834"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSansMono",
|
||||
"encoding": "4878"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4906"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4929"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "4971"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5001"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5030"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5052"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5083"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5116"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5143"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5175"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5207"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5243"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "Cmr10",
|
||||
"encoding": "5263"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "Cmex10",
|
||||
"encoding": "5270"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "Cmsy10",
|
||||
"encoding": "5275"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "Cmmi10",
|
||||
"encoding": "5280"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5295"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans-Oblique",
|
||||
"encoding": "5313"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSansDisplay",
|
||||
"encoding": "5319"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5334"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5370"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5399"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5427"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5459"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5486"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5513"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5554"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5601"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5647"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5694"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5732"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5771"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5803"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5861"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5904"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5924"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "5951"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "6084"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "6445"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7195"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7409"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7474"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7708"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7747"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7885"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "9029"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "9617"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "10460"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "11445"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans-Bold",
|
||||
"encoding": "11486"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "11497"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "11543"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12280"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12301"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12350"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12372"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12395"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "12416"
|
||||
},
|
||||
{
|
||||
"source": "01_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "13324"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "3214"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "3251"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "7190"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "9937"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "10792"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "10852"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "14712"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "18396"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "18719"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans-Bold",
|
||||
"encoding": "18741"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "18778"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "18804"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "20974"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "20993"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "21093"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "21117"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "21141"
|
||||
},
|
||||
{
|
||||
"source": "02_Matplotlib.pdf",
|
||||
"fontName": "DejaVuSans",
|
||||
"encoding": "21174"
|
||||
},
|
||||
{
|
||||
"source": "03_handout-beginner.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "17"
|
||||
},
|
||||
{
|
||||
"source": "03_handout-beginner.pdf",
|
||||
"fontName": "EVICAO+DejaVuSans-Bold",
|
||||
"encoding": "133"
|
||||
},
|
||||
{
|
||||
"source": "03_handout-beginner.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "152"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "13"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "85"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "104"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "121"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "135"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "159"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "179"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "198"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "NVMZUP+SourceCodePro-Regular",
|
||||
"encoding": "208"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "BMQQDV+DejaVuSans",
|
||||
"encoding": "231"
|
||||
},
|
||||
{
|
||||
"source": "04_handout-intermediate.pdf",
|
||||
"fontName": "NVMZUP+SourceCodePro-Regular",
|
||||
"encoding": "241"
|
||||
},
|
||||
{
|
||||
"source": "07_matplotlib.pdf",
|
||||
"fontName": "SauceCodePowerline-Bold",
|
||||
"encoding": "22"
|
||||
},
|
||||
{
|
||||
"source": "07_matplotlib.pdf",
|
||||
"fontName": "SauceCodePowerline-Regular",
|
||||
"encoding": "47"
|
||||
},
|
||||
{
|
||||
"source": "07_matplotlib.pdf",
|
||||
"fontName": "SauceCodePowerline-Regular",
|
||||
"encoding": "65"
|
||||
},
|
||||
{
|
||||
"source": "07_matplotlib.pdf",
|
||||
"fontName": "SauceCodePowerline-Bold",
|
||||
"encoding": "110"
|
||||
},
|
||||
{
|
||||
"source": "08_matplotlib.pdf",
|
||||
"fontName": "F36",
|
||||
"encoding": "12"
|
||||
},
|
||||
{
|
||||
"source": "08_matplotlib.pdf",
|
||||
"fontName": "F59",
|
||||
"encoding": "42"
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user