Compare commits
34 Commits
bug/rememb
...
Frooodle-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d19d87b8f0 | ||
|
|
a22ce69bc3 | ||
|
|
59b60ebfb8 | ||
|
|
a6ae3734ca | ||
|
|
b9a014b5c7 | ||
|
|
9e30918aae | ||
|
|
c239d95131 | ||
|
|
d591874da6 | ||
|
|
6c623d8d84 | ||
|
|
e059caa14e | ||
|
|
8eab35761d | ||
|
|
c43af24ffe | ||
|
|
e1b3cc736c | ||
|
|
0fb9e18636 | ||
|
|
5e1aac0b84 | ||
|
|
60bf649260 | ||
|
|
a58696a38e | ||
|
|
44abc67678 | ||
|
|
d1e690ff8d | ||
|
|
5dc8fa08ee | ||
|
|
db028dfe27 | ||
|
|
c24c504350 | ||
|
|
5dcfe64d1c | ||
|
|
d843696703 | ||
|
|
67de8a9460 | ||
|
|
b26aa3417e | ||
|
|
8dfb5940ca | ||
|
|
0ce479e1e3 | ||
|
|
cca3b6b525 | ||
|
|
03529567ba | ||
|
|
781a52c759 | ||
|
|
be2c103065 | ||
|
|
80fd2eff5f | ||
|
|
65abfd9c7a |
179
.github/workflows/PR-Demo-Comment.yml
vendored
Normal file
179
.github/workflows/PR-Demo-Comment.yml
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
name: PR Deployment via Comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
github.event.comment.user.login == 'frooodle' ||
|
||||
github.event.comment.user.login == 'sf298' ||
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'LaserKaspar' ||
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne'
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
|
||||
steps:
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.issue.number;
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// For forks, use the full repository name, for internal PRs use the current repo
|
||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||
|
||||
console.log(`PR Repository: ${repository}`);
|
||||
console.log(`PR Branch: ${pr.head.ref}`);
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
deploy-pr:
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
# First create the docker-compose content locally
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
ports:
|
||||
- "${{ needs.check-comment.outputs.pr_number }}:8080"
|
||||
volumes:
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
|
||||
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Then copy the file and execute commands
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
# Create PR-specific directories
|
||||
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Start or restart the container
|
||||
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
ENDSSH
|
||||
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
|
||||
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
||||
`Your PR has been deployed for testing!\n\n` +
|
||||
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
78
.github/workflows/PR-Demo-cleanup.yml
vendored
Normal file
78
.github/workflows/PR-Demo-cleanup.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: PR Deployment cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
||||
CLEANUP_PERFORMED: 'false' # Add flag to track if cleanup occurred
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed'
|
||||
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup PR deployment
|
||||
id: cleanup
|
||||
run: |
|
||||
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove containers
|
||||
cd /stirling/PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove PR-specific directories
|
||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Remove the Docker image
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "PERFORMED_CLEANUP"
|
||||
else
|
||||
echo "PR directory not found, nothing to clean up"
|
||||
echo "NO_CLEANUP_NEEDED"
|
||||
fi
|
||||
ENDSSH
|
||||
)
|
||||
|
||||
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
|
||||
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cleanup_performed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post cleanup notice to PR
|
||||
if: steps.cleanup.outputs.cleanup_performed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
const commentBody = `## 🧹 Deployment Cleanup\n\n` +
|
||||
`The test deployment for this PR has been cleaned up.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
52
.github/workflows/check_properties.yml
vendored
52
.github/workflows/check_properties.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
paths:
|
||||
- "src/main/resources/messages_en_GB.properties"
|
||||
|
||||
# Permissions required for the workflow
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -18,6 +19,13 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -26,13 +34,6 @@ jobs:
|
||||
path: pr-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -49,34 +50,46 @@ jobs:
|
||||
echo "Fetching PR changed files..."
|
||||
cd pr-branch
|
||||
gh repo set-default ${{ github.repository }}
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
|
||||
# Store files in a safe way, only allowing valid properties files
|
||||
echo "Getting list of changed files from PR..."
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
|
||||
cd ..
|
||||
echo $(cat changed_files.txt)
|
||||
echo "Setting branch path..."
|
||||
BRANCH_PATH="pr-branch"
|
||||
|
||||
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
|
||||
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
|
||||
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
echo "Processing changed files..."
|
||||
mapfile -t CHANGED_FILES < changed_files.txt
|
||||
|
||||
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
|
||||
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
|
||||
|
||||
echo "Changed files: ${CHANGED_FILES_STR}"
|
||||
echo "Branch: ${BRANCH_PATH}"
|
||||
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
run: |
|
||||
echo "Determining reference file..."
|
||||
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
|
||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
||||
echo "Using PR branch reference file"
|
||||
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using main branch reference file"
|
||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
|
||||
|
||||
- name: Show REFERENCE_FILE
|
||||
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
|
||||
run: echo "Reference file is set to ${REFERENCE_FILE}"
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
|
||||
echo "Running Python script to check files..."
|
||||
python main-branch/.github/scripts/check_language_properties.py \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch "${BRANCH_PATH}" \
|
||||
--files ${CHANGED_FILES} > failure.txt || true
|
||||
|
||||
- name: Capture output
|
||||
id: capture-output
|
||||
@@ -87,7 +100,7 @@ jobs:
|
||||
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo $ERROR_OUTPUT
|
||||
echo "${ERROR_OUTPUT}"
|
||||
else
|
||||
echo "No errors found."
|
||||
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
|
||||
@@ -169,7 +182,10 @@ jobs:
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
|
||||
echo "Running Python script to check files..."
|
||||
python .github/scripts/check_language_properties.py \
|
||||
--reference-file src/main/resources/messages_en_GB.properties \
|
||||
--branch main
|
||||
|
||||
- name: Set up git config
|
||||
run: |
|
||||
|
||||
3
.github/workflows/push-docker.yml
vendored
3
.github/workflows/push-docker.yml
vendored
@@ -67,6 +67,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -95,6 +96,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -122,6 +124,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
@@ -114,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -173,20 +173,20 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
```
|
||||
|
||||
For the ultra-lite version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with security enabled):
|
||||
|
||||
```bash
|
||||
export DOCKER_ENABLE_SECURITY=true
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
45
Jenkinsfile
vendored
45
Jenkinsfile
vendored
@@ -1,45 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'chmod 755 gradlew'
|
||||
sh './gradlew build'
|
||||
}
|
||||
}
|
||||
stage('Docker Build') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
sh "docker build -t $image ."
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Docker Push') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "docker push $image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Helm Push') {
|
||||
steps {
|
||||
script {
|
||||
//TODO: Read chartVersion from Chart.yaml
|
||||
def chartVersion = '1.0.0'
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "helm package chart/stirling-pdf"
|
||||
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
README.md
38
README.md
@@ -120,13 +120,13 @@ Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/b
|
||||
### Docker / Podman
|
||||
|
||||
> [!NOTE]
|
||||
> <https://hub.docker.com/r/frooodle/s-pdf>
|
||||
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
|
||||
|
||||
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
|
||||
|
||||
@@ -144,7 +144,7 @@ docker run -d \
|
||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||
-e LANGS=en_GB \
|
||||
--name stirling-pdf \
|
||||
frooodle/s-pdf:latest
|
||||
stirlingtools/stirling-pdf:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
@@ -153,7 +153,7 @@ docker run -d \
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
@@ -190,29 +190,29 @@ Stirling-PDF currently supports 36 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| 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) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
@@ -225,7 +225,7 @@ Stirling-PDF currently supports 36 languages!
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
|
||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ plugins {
|
||||
//id "nebula.lint" version "19.0.3"
|
||||
}
|
||||
|
||||
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
|
||||
ext {
|
||||
@@ -17,12 +19,12 @@ ext {
|
||||
pdfboxVersion = "3.0.3"
|
||||
logbackVersion = "1.5.7"
|
||||
imageioVersion = "3.12.0"
|
||||
lombokVersion = "1.18.34"
|
||||
lombokVersion = "1.18.36"
|
||||
bouncycastleVersion = "1.78.1"
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.32.0"
|
||||
version = "0.33.1"
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat
|
||||
image: frooodle/s-pdf:latest-fat
|
||||
image: stirlingtools/stirling-pdf:latest-fat
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -32,12 +32,12 @@ ignore = [
|
||||
ignore = [
|
||||
'AddStampRequest.alphabet',
|
||||
'AddStampRequest.position',
|
||||
'home.pipeline.title'
|
||||
'PDFToBook.selectText.1',
|
||||
'PDFToText.tags',
|
||||
'addPageNumbers.selectText.3',
|
||||
'alphabet',
|
||||
'certSign.name',
|
||||
'home.pipeline.title',
|
||||
'language.direction',
|
||||
'licenses.version',
|
||||
'pipeline.title',
|
||||
@@ -46,7 +46,6 @@ ignore = [
|
||||
'sponsor',
|
||||
'text',
|
||||
'watermark.type.1',
|
||||
'certSign.name',
|
||||
]
|
||||
|
||||
[el_GR]
|
||||
|
||||
@@ -34,6 +34,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
@Value("${spring.datasource.url}")
|
||||
private String url;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String databaseUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String databasePassword;
|
||||
|
||||
private Path backupPath = Paths.get("configs/db/backup/");
|
||||
|
||||
@Override
|
||||
@@ -134,7 +140,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
@@ -147,7 +154,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
// Retrieves the H2 database version.
|
||||
public String getH2Version() {
|
||||
String version = "Unknown";
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||
if (rs.next()) {
|
||||
@@ -189,7 +197,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
private boolean executeDatabaseScript(Path scriptPath) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, scriptPath.toString());
|
||||
stmt.execute();
|
||||
|
||||
@@ -32,11 +32,19 @@ public class MetricsAggregatorService {
|
||||
.counters()
|
||||
.forEach(
|
||||
counter -> {
|
||||
String key =
|
||||
String.format(
|
||||
"http_requests_%s_%s",
|
||||
counter.getId().getTag("method"),
|
||||
counter.getId().getTag("uri").replace("/", "_"));
|
||||
String method = counter.getId().getTag("method");
|
||||
String uri = counter.getId().getTag("uri");
|
||||
|
||||
// Skip if either method or uri is null
|
||||
if (method == null || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = String.format(
|
||||
"http_requests_%s_%s",
|
||||
method,
|
||||
uri.replace("/", "_")
|
||||
);
|
||||
|
||||
double currentCount = counter.count();
|
||||
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||
|
||||
@@ -81,6 +81,7 @@ page=صفحة
|
||||
pages=صفحات
|
||||
loading=جارٍ التحميل...
|
||||
addToDoc=إضافة إلى المستند
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=سياسة الخصوصية
|
||||
legal.terms=شروط الاستخدام
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Страница
|
||||
pages=Страници
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Политика за поверителност
|
||||
legal.terms=Правила и условия
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Pàgina
|
||||
pages=Pàgines
|
||||
loading=Carregant...
|
||||
addToDoc=Afegeix al document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacitat
|
||||
legal.terms=Termes i condicions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Strana
|
||||
pages=Strany
|
||||
loading=Načítání...
|
||||
addToDoc=Přidat do dokumentu
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Politika soukromí
|
||||
legal.terms=Podmínky použití
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Sidenummer
|
||||
pages=Sideantal
|
||||
loading=Laster...
|
||||
addToDoc=Tilføj til Dokument
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Vilkår og betingelser
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Seite
|
||||
pages=Seiten
|
||||
loading=Laden...
|
||||
addToDoc=In Dokument hinzufügen
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Datenschutz
|
||||
legal.terms=AGB
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Σελίδα
|
||||
pages=Σελίδες
|
||||
loading=Φόρτωση...
|
||||
addToDoc=Πρόσθεση στο Εκπομπώματο
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Πολιτική Προνομίους
|
||||
legal.terms=Φράσεις Υποχρεωτικότητας
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Página
|
||||
pages=Páginas
|
||||
loading=Cargando...
|
||||
addToDoc=Agregar al Documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacidad
|
||||
legal.terms=Términos y Condiciones
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Chargement...
|
||||
addToDoc=Ajouter au Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Politique de Confidentialité
|
||||
legal.terms=Conditions Générales
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=पृष्ठ
|
||||
pages=पृष्ठों
|
||||
loading=डालिंग...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=गुप्तता सूचना
|
||||
legal.terms=शर्तें और प्रवाह
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Stranica
|
||||
pages=Stranice
|
||||
loading=Učitavanje...
|
||||
addToDoc=Dodaj u dokument
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Politika privatnosti
|
||||
legal.terms=Uspe sodržine
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Oldal
|
||||
pages=Oldalak
|
||||
loading=Betöltés...
|
||||
addToDoc=Hozzáadás dokumentumba
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Adatvédelmi nyilatkozat
|
||||
legal.terms=Feltételek és feltételek
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Halaman
|
||||
pages=Halaman-halaman
|
||||
loading=Mengambil data...
|
||||
addToDoc=Tambahkan ke Dokumen
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Kebijakan Privasi
|
||||
legal.terms=Syarat dan Ketentuan
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Pagina
|
||||
pages=Pagine
|
||||
loading=Caricamento...
|
||||
addToDoc=Aggiungi al documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Informativa sulla privacy
|
||||
legal.terms=Termini e Condizioni
|
||||
@@ -141,7 +142,7 @@ navbar.language=Lingue
|
||||
navbar.settings=Impostazioni
|
||||
navbar.allTools=Strumenti
|
||||
navbar.multiTool=Strumenti multipli
|
||||
navbar.search=Search
|
||||
navbar.search=Cerca
|
||||
navbar.sections.organize=Organizza
|
||||
navbar.sections.convertTo=Converti in PDF
|
||||
navbar.sections.convertFrom=Converti da PDF
|
||||
@@ -944,7 +945,7 @@ multiTool.downloadAll=Esporta
|
||||
multiTool.downloadSelected=Esporta selezionata
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
|
||||
|
||||
#view pdf
|
||||
viewPdf.title=Visualizza PDF
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=プライバシーポリシー
|
||||
legal.terms=利用規約
|
||||
|
||||
@@ -81,6 +81,7 @@ page=페이지
|
||||
pages=페이지
|
||||
loading=로딩 중...
|
||||
addToDoc=문서에 추가
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=개인 정보 정책
|
||||
legal.terms=이용 약관
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Pagina
|
||||
pages=Pagen
|
||||
loading=Laden...
|
||||
addToDoc=Toevoegen aan document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacybeleid
|
||||
legal.terms=Voorwaarden van gebruik
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Strona
|
||||
pages=Strony
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Polityka Prywatności
|
||||
legal.terms=Zasady i Postanowienia
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Página
|
||||
pages=Páginas
|
||||
loading=Carregando...
|
||||
addToDoc=Adicionar ao Documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacidade
|
||||
legal.terms=Termos e Condições
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Página
|
||||
pages=Páginas
|
||||
loading=A carregar...
|
||||
addToDoc=Adicionar ao Documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacidade
|
||||
legal.terms=Termos e Condições
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Страница
|
||||
pages=Страницы
|
||||
loading=Загрузка...
|
||||
addToDoc=Добавить в документ
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Политика конфиденциальности
|
||||
legal.terms=Условия использования
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Sidan
|
||||
pages=Sidor
|
||||
loading=Laddar...
|
||||
addToDoc=Lägg till i dokument
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Dataprotektionspolicy
|
||||
legal.terms=Villkor och betingelser
|
||||
|
||||
@@ -81,6 +81,7 @@ page=หน้า
|
||||
pages=หน้า
|
||||
loading=กำลังโหลด...
|
||||
addToDoc=เพิ่มเข้าสู่เอกสาร
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=นโยบายความเป็นส่วนตัว
|
||||
legal.terms=ข้อกำหนดการใช้งาน
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Gizlilik Politikası
|
||||
legal.terms=Şartlar ve koşullar
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
|
||||
@@ -81,6 +81,7 @@ page=頁面
|
||||
pages=頁面
|
||||
loading=載入中...
|
||||
addToDoc=新增至文件
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=隱私權政策
|
||||
legal.terms=使用條款
|
||||
|
||||
@@ -140,7 +140,7 @@ span.icon-text::after {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-text {
|
||||
/* .tooltip-text {
|
||||
visibility: hidden;
|
||||
background-color: rgb(71 67 67 / 80%);
|
||||
color: #fff;
|
||||
@@ -162,7 +162,7 @@ span.icon-text::after {
|
||||
.nav-item:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
} */
|
||||
|
||||
.dropdown-menu.scrollable-y {
|
||||
overflow-y: scroll;
|
||||
@@ -324,6 +324,18 @@ span.icon-text::after {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* .icon-hide {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
@media (max-width:1199px) {
|
||||
.icon-hide {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1200px) {
|
||||
.icon-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -96,14 +96,31 @@
|
||||
});
|
||||
});
|
||||
|
||||
async function getPDFPageCount(file) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs'
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
return pdf.numPages;
|
||||
} catch (error) {
|
||||
console.error('Error getting PDF page count:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
|
||||
const startTime = performance.now();
|
||||
const file = formData.get('fileInput');
|
||||
let success = false;
|
||||
let errorMessage = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: "POST", body: formData });
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
if (!response.ok) {
|
||||
errorMessage = response.status;
|
||||
if (response.status === 401) {
|
||||
// Handle 401 Unauthorized error
|
||||
showSessionExpiredPrompt();
|
||||
return;
|
||||
}
|
||||
@@ -118,6 +135,8 @@
|
||||
let filename = getFilenameFromContentDisposition(contentDisposition);
|
||||
|
||||
const blob = await response.blob();
|
||||
success = true;
|
||||
|
||||
if (contentType.includes("application/pdf") || contentType.includes("image/")) {
|
||||
clearFileInput();
|
||||
return handleResponse(blob, filename, !isMulti, isZip);
|
||||
@@ -127,13 +146,29 @@
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
success = false;
|
||||
errorMessage = error.message;
|
||||
clearFileInput();
|
||||
console.error("Error in handleSingleDownload:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
const processingTime = performance.now() - startTime;
|
||||
|
||||
// Capture analytics
|
||||
const pageCount = file && file.type === 'application/pdf' ? await getPDFPageCount(file) : null;
|
||||
|
||||
posthog.capture('file_processing', {
|
||||
success: success,
|
||||
file_type: file ? file.type || 'unknown' : 'unknown',
|
||||
file_size: file ? file.size : 0,
|
||||
processing_time: processingTime,
|
||||
error_message: errorMessage,
|
||||
pdf_pages: pageCount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFilenameFromContentDisposition(contentDisposition) {
|
||||
function getFilenameFromContentDisposition(contentDisposition) {
|
||||
let filename;
|
||||
|
||||
if (contentDisposition && contentDisposition.indexOf("attachment") !== -1) {
|
||||
@@ -308,14 +343,14 @@
|
||||
if(editSectionElement){
|
||||
editSectionElement.style.display = "none";
|
||||
}
|
||||
let cropPdfCanvas = document.querySelector("#crop-pdf-canvas");
|
||||
let cropPdfCanvas = document.querySelector("#cropPdfCanvas");
|
||||
let overlayCanvas = document.querySelector("#overlayCanvas");
|
||||
if(cropPdfCanvas && overlayCanvas){
|
||||
cropPdfCanvas.width = 0;
|
||||
cropPdfCanvas.heigth = 0;
|
||||
cropPdfCanvas.height = 0;
|
||||
|
||||
overlayCanvas.width = 0;
|
||||
overlayCanvas.heigth = 0;
|
||||
overlayCanvas.height = 0;
|
||||
}
|
||||
} else{
|
||||
console.log("Disabled for 'Merge'");
|
||||
|
||||
@@ -9,81 +9,81 @@ const DraggableUtils = {
|
||||
|
||||
init() {
|
||||
interact(".draggable-canvas")
|
||||
.draggable({
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
const target = event.target;
|
||||
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
|
||||
.draggable({
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
const target = event.target;
|
||||
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
|
||||
+ event.dx;
|
||||
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
|
||||
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
|
||||
+ event.dy;
|
||||
|
||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
target.setAttribute("data-bs-x", x);
|
||||
target.setAttribute("data-bs-y", y);
|
||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
target.setAttribute("data-bs-x", x);
|
||||
target.setAttribute("data-bs-y", y);
|
||||
|
||||
this.onInteraction(target);
|
||||
//update the last interacted element
|
||||
this.lastInteracted = event.target;
|
||||
this.onInteraction(target);
|
||||
//update the last interacted element
|
||||
this.lastInteracted = event.target;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.resizable({
|
||||
edges: { left: true, right: true, bottom: true, top: true },
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
var target = event.target;
|
||||
var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
|
||||
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
|
||||
})
|
||||
.resizable({
|
||||
edges: { left: true, right: true, bottom: true, top: true },
|
||||
listeners: {
|
||||
move: (event) => {
|
||||
var target = event.target;
|
||||
var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
|
||||
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
|
||||
|
||||
// check if control key is pressed
|
||||
if (event.ctrlKey) {
|
||||
const aspectRatio = target.offsetWidth / target.offsetHeight;
|
||||
// preserve aspect ratio
|
||||
let width = event.rect.width;
|
||||
let height = event.rect.height;
|
||||
// check if control key is pressed
|
||||
if (event.ctrlKey) {
|
||||
const aspectRatio = target.offsetWidth / target.offsetHeight;
|
||||
// preserve aspect ratio
|
||||
let width = event.rect.width;
|
||||
let height = event.rect.height;
|
||||
|
||||
if (Math.abs(event.deltaRect.width) >= Math.abs(
|
||||
if (Math.abs(event.deltaRect.width) >= Math.abs(
|
||||
event.deltaRect.height)) {
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
width = height * aspectRatio;
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
|
||||
event.rect.width = width;
|
||||
event.rect.height = height;
|
||||
}
|
||||
|
||||
event.rect.width = width;
|
||||
event.rect.height = height;
|
||||
}
|
||||
target.style.width = event.rect.width + "px";
|
||||
target.style.height = event.rect.height + "px";
|
||||
|
||||
target.style.width = event.rect.width + "px";
|
||||
target.style.height = event.rect.height + "px";
|
||||
// translate when resizing from top or left edges
|
||||
x += event.deltaRect.left;
|
||||
y += event.deltaRect.top;
|
||||
|
||||
// translate when resizing from top or left edges
|
||||
x += event.deltaRect.left;
|
||||
y += event.deltaRect.top;
|
||||
target.style.transform = "translate(" + x + "px," + y + "px)";
|
||||
|
||||
target.style.transform = "translate(" + x + "px," + y + "px)";
|
||||
|
||||
target.setAttribute("data-bs-x", x);
|
||||
target.setAttribute("data-bs-y", y);
|
||||
target.textContent = Math.round(event.rect.width) + "\u00D7"
|
||||
target.setAttribute("data-bs-x", x);
|
||||
target.setAttribute("data-bs-y", y);
|
||||
target.textContent = Math.round(event.rect.width) + "\u00D7"
|
||||
+ Math.round(event.rect.height);
|
||||
|
||||
this.onInteraction(target);
|
||||
this.onInteraction(target);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
modifiers: [
|
||||
interact.modifiers.restrictSize({
|
||||
min: {width: 5, height: 5},
|
||||
}),
|
||||
],
|
||||
inertia: true,
|
||||
});
|
||||
modifiers: [
|
||||
interact.modifiers.restrictSize({
|
||||
min: { width: 5, height: 5 },
|
||||
}),
|
||||
],
|
||||
inertia: true,
|
||||
});
|
||||
//Arrow key Support for Add-Image and Sign pages
|
||||
if(window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
|
||||
if (window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
//Check for last interacted element
|
||||
if (!this.lastInteracted){
|
||||
if (!this.lastInteracted) {
|
||||
return;
|
||||
}
|
||||
// Get the currently selected element
|
||||
@@ -288,7 +288,7 @@ const DraggableUtils = {
|
||||
}
|
||||
},
|
||||
|
||||
parseTransform(element) {},
|
||||
parseTransform(element) { },
|
||||
async getOverlayedPdfDocument() {
|
||||
const pdfBytes = await this.pdfDoc.getData();
|
||||
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
|
||||
@@ -308,6 +308,7 @@ const DraggableUtils = {
|
||||
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
|
||||
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
|
||||
|
||||
|
||||
for (const draggableData of draggablesData) {
|
||||
// embed the draggable canvas
|
||||
const draggableElement = draggableData.element;
|
||||
@@ -324,6 +325,24 @@ const DraggableUtils = {
|
||||
width: draggableData.offsetWidth,
|
||||
height: draggableData.offsetHeight,
|
||||
};
|
||||
|
||||
//Auxiliary variables
|
||||
let widthAdjusted = page.getWidth();
|
||||
let heightAdjusted = page.getHeight();
|
||||
const rotation = page.getRotation();
|
||||
|
||||
//Normalizing angle
|
||||
let normalizedAngle = rotation.angle % 360;
|
||||
if (normalizedAngle < 0) {
|
||||
normalizedAngle += 360;
|
||||
}
|
||||
|
||||
//Changing the page dimension if the angle is 90 or 270
|
||||
if (normalizedAngle === 90 || normalizedAngle === 270) {
|
||||
let widthTemp = widthAdjusted;
|
||||
widthAdjusted = heightAdjusted;
|
||||
heightAdjusted = widthTemp;
|
||||
}
|
||||
const draggablePositionRelative = {
|
||||
x: draggablePositionPixels.x / offsetWidth,
|
||||
y: draggablePositionPixels.y / offsetHeight,
|
||||
@@ -331,18 +350,36 @@ const DraggableUtils = {
|
||||
height: draggablePositionPixels.height / offsetHeight,
|
||||
};
|
||||
const draggablePositionPdf = {
|
||||
x: draggablePositionRelative.x * page.getWidth(),
|
||||
y: draggablePositionRelative.y * page.getHeight(),
|
||||
width: draggablePositionRelative.width * page.getWidth(),
|
||||
height: draggablePositionRelative.height * page.getHeight(),
|
||||
x: draggablePositionRelative.x * widthAdjusted,
|
||||
y: draggablePositionRelative.y * heightAdjusted,
|
||||
width: draggablePositionRelative.width * widthAdjusted,
|
||||
height: draggablePositionRelative.height * heightAdjusted,
|
||||
};
|
||||
|
||||
//Defining the image if the page has a 0-degree angle
|
||||
let x = draggablePositionPdf.x
|
||||
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
|
||||
|
||||
|
||||
//Defining the image position if it is at other angles
|
||||
if (normalizedAngle === 90) {
|
||||
x = draggablePositionPdf.y + draggablePositionPdf.height;
|
||||
y = draggablePositionPdf.x;
|
||||
} else if (normalizedAngle === 180) {
|
||||
x = widthAdjusted - draggablePositionPdf.x;
|
||||
y = draggablePositionPdf.y + draggablePositionPdf.height;
|
||||
} else if (normalizedAngle === 270) {
|
||||
x = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
|
||||
y = widthAdjusted - draggablePositionPdf.x;
|
||||
}
|
||||
|
||||
// draw the image
|
||||
page.drawImage(pdfImageObject, {
|
||||
x: draggablePositionPdf.x,
|
||||
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
|
||||
x: x,
|
||||
y: y,
|
||||
width: draggablePositionPdf.width,
|
||||
height: draggablePositionPdf.height,
|
||||
rotate: rotation
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,23 +23,26 @@
|
||||
</form>
|
||||
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
|
||||
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<div style="position: relative; width: auto;" id="canvasesContainer">
|
||||
<div>
|
||||
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
|
||||
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> < </button>
|
||||
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> > </button>
|
||||
</div>
|
||||
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
|
||||
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
|
||||
</div>
|
||||
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
|
||||
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script>
|
||||
let pdfCanvas = document.getElementById('crop-pdf-canvas');
|
||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
|
||||
let overlayCanvas = document.getElementById('overlayCanvas');
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
canvasesContainer.style.display = "none";
|
||||
// let paginationBtnContainer = ;
|
||||
|
||||
let context = pdfCanvas.getContext('2d');
|
||||
let overlayContext = overlayCanvas.getContext('2d');
|
||||
|
||||
let btn1Object = document.getElementById('previous-page-btn');
|
||||
let btn2Object = document.getElementById('next-page-btn');
|
||||
@@ -60,6 +63,8 @@
|
||||
let rectWidth = 0;
|
||||
let rectHeight = 0;
|
||||
|
||||
let timeId = null; // timeout id for resizing canvases event
|
||||
|
||||
btn1Object.addEventListener('click',function (e){
|
||||
if (currentPage !== 1) {
|
||||
currentPage = currentPage - 1;
|
||||
@@ -102,14 +107,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
file = e.target.files[0];
|
||||
function renderPageFromFile(file) {
|
||||
if (file.type === 'application/pdf') {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
|
||||
pdfDoc = pdf;
|
||||
totalPages = pdf.numPages;
|
||||
renderPage(currentPage);
|
||||
@@ -117,9 +121,37 @@
|
||||
pageId.value = currentPage;
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
document.getElementById("pagination-button-container").style.display="flex";
|
||||
document.getElementById("instruction-text").style.display="block";
|
||||
document.getElementById("pagination-button-container").style.display = "flex";
|
||||
document.getElementById("instruction-text").style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", function() {
|
||||
clearTimeout(timeId);
|
||||
timeId = setTimeout(function () {
|
||||
if (fileInput.files.length == 0) return;
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
let containerRect = canvasesContainer.getBoundingClientRect();
|
||||
|
||||
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
pdfCanvas.width = containerRect.width;
|
||||
pdfCanvas.height = containerRect.height;
|
||||
|
||||
overlayCanvas.width = containerRect.width;
|
||||
overlayCanvas.height = containerRect.height;
|
||||
|
||||
let file = fileInput.files[0];
|
||||
renderPageFromFile(file);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
canvasesContainer.style.display = "block"; // set for visual purposes
|
||||
file = e.target.files[0];
|
||||
renderPageFromFile(file);
|
||||
});
|
||||
|
||||
function renderPage(pageNumber) {
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
|
||||
})();
|
||||
</script>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script th:src="@{'/js/downloader.js'}"></script>
|
||||
|
||||
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
<span class="material-symbols-rounded" id="dark-mode-icon">
|
||||
dark_mode
|
||||
</span>
|
||||
<span class="icon-text icon-hide tooltip-text" id="dark-mode-text" th:data-text="#{navbar.darkmode}" th:text="#{navbar.darkmode}"></span>
|
||||
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}" th:text="#{navbar.darkmode}"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
@@ -380,7 +380,7 @@
|
||||
<span class="material-symbols-rounded">
|
||||
settings
|
||||
</span>
|
||||
<span class="icon-text icon-hide tooltip-text" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span>
|
||||
<span class="icon-text icon-hide" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -411,7 +411,7 @@
|
||||
th:title="#{visitGithub}">
|
||||
<img th:src="@{'/images/github.svg'}" alt="github">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/frooodle/s-pdf" class="mx-1" role="button" target="_blank"th:title="#{seeDockerHub}">
|
||||
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"th:title="#{seeDockerHub}">
|
||||
<img th:src="@{'/images/docker.svg'}" alt="docker">
|
||||
</a>
|
||||
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank" th:title="#{joinDiscord}">
|
||||
|
||||
@@ -83,9 +83,6 @@
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="resetFileInputBtn" class="btn btn-danger" onclick="removeAllElements()" th:text="#{reset}">Reset</button>
|
||||
</div>
|
||||
<div id="selected-pages-display" class="selected-pages-container hidden">
|
||||
<div style="display:flex; height:3rem; margin-right:1rem">
|
||||
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
|
||||
|
||||
2
test2.sh
2
test2.sh
@@ -49,7 +49,7 @@ check_health() {
|
||||
build_and_test() {
|
||||
local version_tag="alpha"
|
||||
local dockerfile_name="./Dockerfile"
|
||||
local image_base="frooodle/s-pdf"
|
||||
local image_base="stirlingtools/stirling-pdf"
|
||||
local security_suffix=""
|
||||
local docker_compose_base="./exampleYmlFiles/docker-compose-latest"
|
||||
local compose_suffix=".yml"
|
||||
|
||||
Reference in New Issue
Block a user