Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba40483e0 | ||
|
|
4b5929331f | ||
|
|
d19f13f137 | ||
|
|
25609dfe6f | ||
|
|
b9acf47530 | ||
|
|
495b844c00 | ||
|
|
98e20ea843 | ||
|
|
4c5accaae4 | ||
|
|
6e8fe9ad72 | ||
|
|
d9cb1cac03 | ||
|
|
5da298ef0b | ||
|
|
23db71c4a8 | ||
|
|
b819162732 | ||
|
|
dc665b9fa4 | ||
|
|
776c4971a6 | ||
|
|
130a3b0d1c | ||
|
|
31871bd16e | ||
|
|
37d507ade6 | ||
|
|
88d0ac815a | ||
|
|
ecb9ec874c | ||
|
|
ee97a7f9be | ||
|
|
5f4db03982 | ||
|
|
4c8d122488 | ||
|
|
05d4861dbb | ||
|
|
3d103cb6f6 | ||
|
|
2ada2b2ea6 | ||
|
|
7e630f9337 | ||
|
|
a59da16b4c | ||
|
|
adccd4314b | ||
|
|
4d570e502b | ||
|
|
d948849915 | ||
|
|
834f066704 | ||
|
|
0aa68bc8e7 | ||
|
|
e9a005b658 | ||
|
|
82f73cfe60 | ||
|
|
032e188a24 | ||
|
|
2dbf8de089 | ||
|
|
1c0459683b | ||
|
|
856a8403b3 | ||
|
|
dc6209dbc1 | ||
|
|
4b540faec4 | ||
|
|
2f6c95dd51 | ||
|
|
beb77d3fe9 | ||
|
|
152f137cad | ||
|
|
1358cb0e6d | ||
|
|
ca54b450de | ||
|
|
2221e6a29c | ||
|
|
db44823ecf |
@@ -7,7 +7,7 @@ args_bin = []
|
|||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "templ generate && go build -o ./tmp/main ."
|
cmd = "templ generate && go build -o ./tmp/main ."
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "docs"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go", "_templ.go"]
|
exclude_regex = ["_test.go", "_templ.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
name: Development Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'development'
|
||||||
|
- 'feature/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ development, feature/** ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- staging
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Use github.repository as the default image name
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
DOCKERHUB_IMAGE: starfleetcptn/gomft
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Development Binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: |
|
||||||
|
# Build JavaScript and CSS assets
|
||||||
|
node build.js
|
||||||
|
|
||||||
|
# Ensure the dist directory exists
|
||||||
|
mkdir -p static/dist
|
||||||
|
|
||||||
|
# Verify the build output
|
||||||
|
ls -la static/dist
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24.x'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
# Install templ compiler for template generation
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
|
||||||
|
- name: Generate template files
|
||||||
|
run: templ generate
|
||||||
|
|
||||||
|
- name: Set Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# For development builds, use branch name or PR number with commit hash
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
VERSION="pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD)"
|
||||||
|
else
|
||||||
|
BRANCH=${GITHUB_REF#refs/heads/}
|
||||||
|
VERSION="${BRANCH//\//-}-$(git rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Also set build timestamp for versioning
|
||||||
|
echo "BUILD_TIME=$(date -u +'%Y-%m-%d_%H:%M:%S')" >> $GITHUB_ENV
|
||||||
|
echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build for Linux (amd64)
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
# Define common ldflags with version information
|
||||||
|
LDFLAGS="-X github.com/starfleetcptn/gomft/components.AppVersion=$VERSION -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.Commit=$COMMIT -X github.com/starfleetcptn/gomft/components.BuildTime=$BUILD_TIME -X github.com/starfleetcptn/gomft/components.Commit=$COMMIT"
|
||||||
|
|
||||||
|
# Only build for Linux amd64 for development builds
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o dist/gomft-$VERSION-linux-amd64 .
|
||||||
|
|
||||||
|
# Create checksums
|
||||||
|
cd dist
|
||||||
|
sha256sum * > SHA256SUMS.txt
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dev-binary
|
||||||
|
path: dist/
|
||||||
|
retention-days: 7 # Keep development builds for 7 days
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Build and Push Development Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
# Set the permissions needed for the GitHub token to push to GHCR
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Set up Node.js for frontend build
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# Build frontend assets
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: |
|
||||||
|
node build.js
|
||||||
|
ls -la static/dist/
|
||||||
|
|
||||||
|
# Set version information
|
||||||
|
- name: Set Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# For development builds, use branch name or PR number with commit hash
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
VERSION="pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD)"
|
||||||
|
else
|
||||||
|
BRANCH=${GITHUB_REF#refs/heads/}
|
||||||
|
VERSION="${BRANCH//\//-}-$(git rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Also set build timestamp for versioning
|
||||||
|
echo "BUILD_TIME=$(date -u +'%Y-%m-%d_%H:%M:%S')" >> $GITHUB_ENV
|
||||||
|
echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Set up Docker Buildx for efficient builds
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Login to GitHub Container Registry
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Login to DockerHub
|
||||||
|
- name: Log in to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Extract metadata for GitHub Container Registry
|
||||||
|
- name: Extract GitHub Container Registry metadata
|
||||||
|
id: meta-ghcr
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=sha,format=short
|
||||||
|
type=raw,value=dev-latest
|
||||||
|
|
||||||
|
# Extract metadata for DockerHub
|
||||||
|
- name: Extract DockerHub metadata
|
||||||
|
id: meta-dockerhub
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.DOCKERHUB_IMAGE }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=sha,format=short
|
||||||
|
type=raw,value=dev-latest
|
||||||
|
|
||||||
|
# Build and push Docker image to both registries
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta-ghcr.outputs.tags }}
|
||||||
|
${{ steps.meta-dockerhub.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-ghcr.outputs.labels }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ env.VERSION }}
|
||||||
|
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||||
|
COMMIT=${{ env.COMMIT }}
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -96,6 +96,7 @@ jobs:
|
|||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ env.VERSION }}
|
VERSION=${{ env.VERSION }}
|
||||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ jobs:
|
|||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ env.VERSION }}
|
VERSION=${{ env.VERSION }}
|
||||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: Deploy Docusaurus to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['docs/**']
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Deploy Docusaurus
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./docs
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ./docs/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Copy screenshots to static directory
|
||||||
|
run: |
|
||||||
|
npm run prepare-screenshots
|
||||||
|
# Ensure the images referenced in the docs are definitely available
|
||||||
|
mkdir -p static/img
|
||||||
|
cp -v ../screenshots/dashboard.gomft.png static/img/
|
||||||
|
cp -v ../screenshots/transfer.config.gomft.png static/img/
|
||||||
|
ls -la static/img/dashboard.gomft.png static/img/transfer.config.gomft.png
|
||||||
|
|
||||||
|
- name: Fix Markdown image paths
|
||||||
|
run: npm run fix-image-paths
|
||||||
|
|
||||||
|
- name: Verify screenshots exist
|
||||||
|
run: |
|
||||||
|
echo "Checking if screenshots were copied correctly..."
|
||||||
|
if [ -f "static/img/dashboard.gomft.png" ]; then
|
||||||
|
echo "✅ Found dashboard.gomft.png in static/img/"
|
||||||
|
else
|
||||||
|
echo "❌ Missing dashboard.gomft.png in static/img/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -f "static/img/transfer.config.gomft.png" ]; then
|
||||||
|
echo "✅ Found transfer.config.gomft.png in static/img/"
|
||||||
|
else
|
||||||
|
echo "❌ Missing transfer.config.gomft.png in static/img/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ./docs/build
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -61,6 +61,9 @@ configs/
|
|||||||
# Ignore the backups directory
|
# Ignore the backups directory
|
||||||
backups/
|
backups/
|
||||||
|
|
||||||
|
# Ignore the tests directory
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
# Ignore Dirs
|
# Ignore Dirs
|
||||||
/source/
|
/source/
|
||||||
/destination/
|
/destination/
|
||||||
@@ -70,4 +73,35 @@ backups/
|
|||||||
static/dist/
|
static/dist/
|
||||||
|
|
||||||
# Ignore binaries
|
# Ignore binaries
|
||||||
gomft
|
gomft
|
||||||
|
|
||||||
|
# Docusaurus files
|
||||||
|
docs/.docusaurus/
|
||||||
|
docs/.cache-loader/
|
||||||
|
docs/build/
|
||||||
|
docs/build-searchindex/
|
||||||
|
docs/static/search/
|
||||||
|
docs/static/js/
|
||||||
|
docs/node_modules/
|
||||||
|
docs/.env*
|
||||||
|
docs/*.log
|
||||||
|
|
||||||
|
# Added by Claude Task Master
|
||||||
|
logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
dev-debug.log
|
||||||
|
# Environment variables
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
# OS specific
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/
|
||||||
@@ -35,6 +35,10 @@ WORKDIR /app
|
|||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG BUILD_TIME=unknown
|
ARG BUILD_TIME=unknown
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT=unknown
|
||||||
|
# Architecture-related build arguments
|
||||||
|
ARG TARGETOS=linux
|
||||||
|
ARG TARGETARCH=amd64
|
||||||
|
ARG TARGETVARIANT=""
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apk add --no-cache git build-base
|
RUN apk add --no-cache git build-base
|
||||||
@@ -65,16 +69,28 @@ COPY . .
|
|||||||
# Generate template files from .templ files
|
# Generate template files from .templ files
|
||||||
RUN templ generate
|
RUN templ generate
|
||||||
|
|
||||||
# Compile the application with version information
|
# Compile the main application with version information
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||||
-ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=${VERSION} -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.Commit=${COMMIT} -X github.com/starfleetcptn/gomft/components.BuildTime=${BUILD_TIME} -X github.com/starfleetcptn/gomft/components.Commit=${COMMIT}" \
|
-ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=${VERSION} -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.Commit=${COMMIT} -X github.com/starfleetcptn/gomft/components.BuildTime=${BUILD_TIME} -X github.com/starfleetcptn/gomft/components.Commit=${COMMIT}" \
|
||||||
-o /app/gomft
|
-o /app/gomft
|
||||||
|
|
||||||
# Install rclone
|
# Compile the command line tool with version information
|
||||||
|
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||||
|
-ldflags "-X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.Commit=${COMMIT}" \
|
||||||
|
-o /app/gomftctl ./cmd/gomftctl
|
||||||
|
|
||||||
|
# Install rclone with appropriate architecture
|
||||||
RUN apk add --no-cache curl unzip && \
|
RUN apk add --no-cache curl unzip && \
|
||||||
curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip && \
|
if [ "$TARGETARCH" = "arm64" ]; then \
|
||||||
unzip rclone-current-linux-amd64.zip && \
|
RCLONE_ARCH="arm64"; \
|
||||||
cd rclone-*-linux-amd64 && \
|
elif [ "$TARGETARCH" = "arm" ]; then \
|
||||||
|
RCLONE_ARCH="arm-v7"; \
|
||||||
|
else \
|
||||||
|
RCLONE_ARCH="amd64"; \
|
||||||
|
fi && \
|
||||||
|
curl -O https://downloads.rclone.org/rclone-current-linux-${RCLONE_ARCH}.zip && \
|
||||||
|
unzip rclone-current-linux-${RCLONE_ARCH}.zip && \
|
||||||
|
cd rclone-*-linux-${RCLONE_ARCH} && \
|
||||||
cp rclone /usr/local/bin/ && \
|
cp rclone /usr/local/bin/ && \
|
||||||
chmod 755 /usr/local/bin/rclone && \
|
chmod 755 /usr/local/bin/rclone && \
|
||||||
cd .. && \
|
cd .. && \
|
||||||
@@ -87,6 +103,8 @@ FROM alpine:3.19
|
|||||||
ARG UID=1000
|
ARG UID=1000
|
||||||
ARG GID=1000
|
ARG GID=1000
|
||||||
ARG USERNAME=gomft
|
ARG USERNAME=gomft
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -99,8 +117,9 @@ RUN apk add --no-cache ca-certificates tzdata sqlite bash shadow su-exec \
|
|||||||
RUN addgroup -g ${GID} ${USERNAME} && \
|
RUN addgroup -g ${GID} ${USERNAME} && \
|
||||||
adduser -D -u ${UID} -G ${USERNAME} -s /bin/sh ${USERNAME}
|
adduser -D -u ${UID} -G ${USERNAME} -s /bin/sh ${USERNAME}
|
||||||
|
|
||||||
# Copy the binary from the builder stage
|
# Copy the binaries from the builder stage
|
||||||
COPY --from=builder /app/gomft /app/
|
COPY --from=builder /app/gomft /app/
|
||||||
|
COPY --from=builder /app/gomftctl /app/
|
||||||
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
|
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
|
||||||
|
|
||||||
# Copy components
|
# Copy components
|
||||||
@@ -117,7 +136,7 @@ RUN mkdir -p /app/data /app/backups
|
|||||||
RUN touch /app/.env && chmod 644 /app/.env && chown ${USERNAME}:${USERNAME} /app/.env
|
RUN touch /app/.env && chmod 644 /app/.env && chown ${USERNAME}:${USERNAME} /app/.env
|
||||||
|
|
||||||
# Set executable permissions
|
# Set executable permissions
|
||||||
RUN chmod +x /app/gomft
|
RUN chmod +x /app/gomft /app/gomftctl
|
||||||
|
|
||||||
# Set ownership of application files
|
# Set ownership of application files
|
||||||
RUN chown -R ${USERNAME}:${USERNAME} /app
|
RUN chown -R ${USERNAME}:${USERNAME} /app
|
||||||
|
|||||||
@@ -6,6 +6,29 @@
|
|||||||
|
|
||||||
GoMFT is a web-based managed file transfer application built with Go, leveraging rclone for robust file transfer capabilities. It provides a user-friendly interface for configuring, scheduling, and monitoring file transfers across various storage providers.
|
GoMFT is a web-based managed file transfer application built with Go, leveraging rclone for robust file transfer capabilities. It provides a user-friendly interface for configuring, scheduling, and monitoring file transfers across various storage providers.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://discord.gg/f9dwtM3j">
|
||||||
|
<img src="https://img.shields.io/discord/1351354052654403675?color=7289da&logo=discord&logoColor=white&label=Discord" alt="Join our Discord server!" />
|
||||||
|
</a>
|
||||||
|
<a href="https://starfleetcptn.github.io/GoMFT/">
|
||||||
|
<img src="https://img.shields.io/badge/docs-online-blue.svg" alt="Documentation" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
Comprehensive documentation is available at [https://starfleetcptn.github.io/GoMFT/](https://starfleetcptn.github.io/GoMFT/).
|
||||||
|
|
||||||
|
The documentation includes:
|
||||||
|
- [Getting Started Guide](https://starfleetcptn.github.io/GoMFT/docs/getting-started)
|
||||||
|
- [Installation Instructions](https://starfleetcptn.github.io/GoMFT/docs/installation)
|
||||||
|
- [Configuration Reference](https://starfleetcptn.github.io/GoMFT/docs/configuration)
|
||||||
|
- [User Guide](https://starfleetcptn.github.io/GoMFT/docs/user-guide)
|
||||||
|
- [Storage Provider Setup](https://starfleetcptn.github.io/GoMFT/docs/storage-providers)
|
||||||
|
- [Advanced Features](https://starfleetcptn.github.io/GoMFT/docs/advanced)
|
||||||
|
- [Troubleshooting](https://starfleetcptn.github.io/GoMFT/docs/troubleshooting)
|
||||||
|
- [API Reference](https://starfleetcptn.github.io/GoMFT/docs/api)
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This application is actively under development. As such, any aspect of the application—including configurations, data structures, and database fields—may change rapidly and without prior notice. Please review all release notes thoroughly before updating.
|
> This application is actively under development. As such, any aspect of the application—including configurations, data structures, and database fields—may change rapidly and without prior notice. Please review all release notes thoroughly before updating.
|
||||||
|
|
||||||
@@ -49,12 +72,6 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
|||||||
- Wasabi
|
- Wasabi
|
||||||
- Local filesystem
|
- Local filesystem
|
||||||
- And more via rclone
|
- And more via rclone
|
||||||
- **Webhook Notifications**: Receive real-time notifications of job events:
|
|
||||||
- Configurable webhook URLs
|
|
||||||
- HMAC-SHA256 authentication with secrets
|
|
||||||
- Custom HTTP headers
|
|
||||||
- Selectable events (job success, job failure)
|
|
||||||
- Detailed JSON payload with job information
|
|
||||||
- **Multiple Notification Services**: Get job status updates through various notification channels:
|
- **Multiple Notification Services**: Get job status updates through various notification channels:
|
||||||
- Email notifications with configurable SMTP settings
|
- Email notifications with configurable SMTP settings
|
||||||
- Webhooks with authentication for custom integrations
|
- Webhooks with authentication for custom integrations
|
||||||
@@ -83,6 +100,7 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
|||||||
- Optimized for both high-volume small files and large file transfers
|
- Optimized for both high-volume small files and large file transfers
|
||||||
- Maximizes bandwidth utilization for cloud storage providers
|
- Maximizes bandwidth utilization for cloud storage providers
|
||||||
- **Web Interface**: User-friendly interface for managing transfers, built with Templ components
|
- **Web Interface**: User-friendly interface for managing transfers, built with Templ components
|
||||||
|
- **Command Line Tools**: Administrative tasks can be performed via the `gomftctl` CLI tool
|
||||||
- **File Pattern Matching**: Support for file patterns to filter files during transfers
|
- **File Pattern Matching**: Support for file patterns to filter files during transfers
|
||||||
- **File Output Patterns**: Dynamic naming of destination files using patterns with date variables
|
- **File Output Patterns**: Dynamic naming of destination files using patterns with date variables
|
||||||
- **Archive Function**: Option to archive transferred files for backup and compliance
|
- **Archive Function**: Option to archive transferred files for backup and compliance
|
||||||
@@ -126,11 +144,18 @@ cd gomft
|
|||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
go mod download
|
go mod download
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Build the application:
|
3. Generate template code:
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build the application and CLI tools:
|
||||||
```bash
|
```bash
|
||||||
go build -o gomft
|
go build -o gomft
|
||||||
|
go build -o gomftctl ./cmd/gomftctl
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Installation
|
### Docker Installation
|
||||||
@@ -221,6 +246,7 @@ services:
|
|||||||
- GOOGLE_CLIENT_ID=your_google_client_id
|
- GOOGLE_CLIENT_ID=your_google_client_id
|
||||||
- GOOGLE_CLIENT_SECRET=your_google_client_secret
|
- GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||||
- TOTP_ENCRYPTION_KEY=your_32_byte_encryption_key_here
|
- TOTP_ENCRYPTION_KEY=your_32_byte_encryption_key_here
|
||||||
|
- GOMFT_ENCRYPTION_KEY=your_32_byte_encryption_key_here
|
||||||
- EMAIL_ENABLED=true
|
- EMAIL_ENABLED=true
|
||||||
- EMAIL_HOST=smtp.example.com
|
- EMAIL_HOST=smtp.example.com
|
||||||
- EMAIL_PORT=587
|
- EMAIL_PORT=587
|
||||||
@@ -246,610 +272,40 @@ docker-compose up -d
|
|||||||
|
|
||||||
For more information and available tags, visit the [GoMFT Docker Hub page](https://hub.docker.com/r/starfleetcptn/gomft).
|
For more information and available tags, visit the [GoMFT Docker Hub page](https://hub.docker.com/r/starfleetcptn/gomft).
|
||||||
|
|
||||||
---
|
## Command Line Tools
|
||||||
|
|
||||||
## Configuration
|
GoMFT includes a command line tool called `gomftctl` for administrative tasks:
|
||||||
|
|
||||||
GoMFT uses an environment file located at `.env` in the root directory of the application. On first run, a default configuration will be created:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# Basic configuration
|
|
||||||
SERVER_ADDRESS=:8080
|
|
||||||
DATA_DIR=/app/data
|
|
||||||
BACKUP_DIR=/app/backups
|
|
||||||
JWT_SECRET=change_this_to_a_secure_random_string
|
|
||||||
BASE_URL=http://localhost:8080
|
|
||||||
|
|
||||||
# Google OAuth configuration (optional, for built-in authentication)
|
|
||||||
GOOGLE_CLIENT_ID=your_google_client_id
|
|
||||||
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
||||||
|
|
||||||
# Email configuration
|
|
||||||
EMAIL_ENABLED=true
|
|
||||||
EMAIL_HOST=smtp.example.com
|
|
||||||
EMAIL_PORT=587
|
|
||||||
EMAIL_FROM_EMAIL=gomft@example.com
|
|
||||||
EMAIL_FROM_NAME=GoMFT
|
|
||||||
EMAIL_REPLY_TO=
|
|
||||||
EMAIL_ENABLE_TLS=true
|
|
||||||
EMAIL_REQUIRE_AUTH=true
|
|
||||||
EMAIL_USERNAME=smtp_username
|
|
||||||
EMAIL_PASSWORD=smtp_password
|
|
||||||
|
|
||||||
# Two-Factor Authentication configuration
|
|
||||||
TOTP_ENCRYPTION_KEY=your_32_byte_encryption_key_here
|
|
||||||
|
|
||||||
# UserID and GroupID
|
|
||||||
PUID=1000
|
|
||||||
PGID=1000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Options
|
|
||||||
|
|
||||||
- `SERVER_ADDRESS`: The address and port to run the server on
|
|
||||||
- `DATA_DIR`: Directory for storing application data (database and configs)
|
|
||||||
- `BACKUP_DIR`: Directory for storing database backups
|
|
||||||
- `JWT_SECRET`: Secret key for JWT token generation
|
|
||||||
- `BASE_URL`: Base URL for generating links in emails (e.g., password reset links)
|
|
||||||
- Google OAuth configuration for built-in authentication:
|
|
||||||
- `GOOGLE_CLIENT_ID`: Your Google OAuth client ID
|
|
||||||
- `GOOGLE_CLIENT_SECRET`: Your Google OAuth client secret
|
|
||||||
- Email configuration settings for system notifications and password resets:
|
|
||||||
- `EMAIL_ENABLED`: Set to `true` to enable email functionality
|
|
||||||
- `EMAIL_HOST`: SMTP server hostname
|
|
||||||
- `EMAIL_PORT`: SMTP server port (usually 587 for TLS, 465 for SSL, or 25 for non-secure)
|
|
||||||
- `EMAIL_USERNAME`: Username for SMTP authentication
|
|
||||||
- `EMAIL_PASSWORD`: Password for SMTP authentication
|
|
||||||
- `EMAIL_FROM_EMAIL`: Email address used as sender
|
|
||||||
- `EMAIL_FROM_NAME`: Name displayed as the sender
|
|
||||||
- `EMAIL_REPLY_TO`: Optional reply-to email address
|
|
||||||
- `EMAIL_ENABLE_TLS`: Set to `true` to use TLS for secure email transmission
|
|
||||||
- `EMAIL_REQUIRE_AUTH`: Set to `true` to require authentication for SMTP connections, or `false` for servers that don't need authentication
|
|
||||||
|
|
||||||
- Two-Factor Authentication (2FA) configuration:
|
|
||||||
- `TOTP_ENCRYPTION_KEY`: Secret key used to encrypt/decrypt TOTP secrets (for 2FA)
|
|
||||||
- Should be exactly 32 bytes (characters) for optimal security
|
|
||||||
- If not set, a default development key will be used (not secure for production)
|
|
||||||
- If shorter than 32 bytes, it will be automatically padded (less secure)
|
|
||||||
- If longer than 32 bytes, it will be truncated to 32 bytes
|
|
||||||
- Example: `TOTP_ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456`
|
|
||||||
|
|
||||||
|
|
||||||
- SSL/TLS Verification Control:
|
|
||||||
- `SKIP_SSL_VERIFY`: Set to `true` to disable SSL/TLS certificate verification for outgoing connections (e.g., webhooks, email). Use with caution, as this can expose connections to man-in-the-middle attacks. Defaults to `false` (verification enabled).
|
|
||||||
- Example: `SKIP_SSL_VERIFY=true`
|
|
||||||
### Logging Configuration
|
|
||||||
|
|
||||||
GoMFT provides configurable logging with rotation support through the following environment variables:
|
|
||||||
|
|
||||||
- `LOGS_DIR`: Directory where log files are stored (default: `./data/logs`)
|
|
||||||
- `LOG_MAX_SIZE`: Maximum size in megabytes for each log file before rotation (default: `10`)
|
|
||||||
- `LOG_MAX_BACKUPS`: Number of old log files to retain (default: `5`)
|
|
||||||
- `LOG_MAX_AGE`: Maximum number of days to retain old log files (default: `30`)
|
|
||||||
- `LOG_COMPRESS`: Whether to compress rotated log files (default: `true`)
|
|
||||||
- `LOG_LEVEL`: Controls verbosity level of logging (values: `error`, `info`, `debug`, default: `info`)
|
|
||||||
- `error`: Only show errors and critical issues
|
|
||||||
- `info`: Show errors and general operational information (default)
|
|
||||||
- `debug`: Show all messages including detailed debugging information
|
|
||||||
|
|
||||||
Log files contain detailed information about file transfers, job execution, and system operations, which can be useful for troubleshooting and auditing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Start the server:
|
|
||||||
```bash
|
```bash
|
||||||
./gomft
|
# View available commands
|
||||||
|
./gomftctl --help
|
||||||
|
|
||||||
|
# Migrate provider data
|
||||||
|
./gomftctl migrate-providers
|
||||||
|
|
||||||
|
# Rotate security keys
|
||||||
|
./gomftctl rotate-key --type jwt
|
||||||
|
|
||||||
|
# Manage users
|
||||||
|
./gomftctl user create --email admin@example.com --password secure_password --admin
|
||||||
|
./gomftctl user list
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
./gomftctl backup
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Access the web interface at `http://localhost:8080`
|
See the [Admin Tools documentation](https://starfleetcptn.github.io/GoMFT/docs/advanced/admin-tools) for more details.
|
||||||
|
|
||||||
3. Log in with the default admin account:
|
|
||||||
- Email: `admin@example.com`
|
|
||||||
- Password: `admin`
|
|
||||||
- **Important**: Change this password immediately after first login
|
|
||||||
|
|
||||||
4. Create transfer configurations:
|
|
||||||
- Navigate to "Transfer Configs" section
|
|
||||||
- Configure source and destination locations with connection details
|
|
||||||
- Set file patterns and archive options as needed
|
|
||||||
- Configure performance settings:
|
|
||||||
- Set "Concurrent Transfers" slider to optimize throughput
|
|
||||||
- Use higher values (8-16) for many small files or fast networks
|
|
||||||
- Use lower values (1-4) for large files or limited bandwidth
|
|
||||||
- Consider source/destination system capabilities when setting
|
|
||||||
|
|
||||||
5. Create jobs using your configurations:
|
|
||||||
- Navigate to "Jobs" section
|
|
||||||
- Select an existing transfer config
|
|
||||||
- Set up a schedule using cron expressions or run manually
|
|
||||||
- Enable/disable as needed
|
|
||||||
|
|
||||||
6. Monitor transfers:
|
|
||||||
- View active and completed transfers on the Dashboard
|
|
||||||
- Check detailed transfer history with performance metrics
|
|
||||||
- View job run details including any error messages
|
|
||||||
|
|
||||||
7. Configure webhook notifications:
|
|
||||||
- Enable webhooks in job settings to receive notifications
|
|
||||||
- Provide a valid webhook URL where notifications will be sent
|
|
||||||
- Optionally set a webhook secret for HMAC-SHA256 signature verification
|
|
||||||
- Configure custom HTTP headers in JSON format if needed
|
|
||||||
- Choose notification triggers (job success, job failure, or both)
|
|
||||||
- Test your webhook integration with manual job runs
|
|
||||||
|
|
||||||
8. **Webhook Notifications**:
|
|
||||||
- **Webhook Integration**: Send notifications to external systems when jobs complete
|
|
||||||
- **Secure Authentication**: HMAC-SHA256 signature for webhook verification
|
|
||||||
- **Custom Headers**: Add custom HTTP headers to webhook requests
|
|
||||||
- **Flexible Configuration**: Configure different webhooks for different jobs
|
|
||||||
- **Event Selection**: Choose to send notifications on success, failure, or both
|
|
||||||
- **Detailed Payload**: Rich JSON payload with complete job execution details
|
|
||||||
|
|
||||||
9. **Multiple Notification Services**:
|
|
||||||
- **Pushbullet Integration**: Send notifications to your devices through Pushbullet
|
|
||||||
- Device targeting support for specific device delivery
|
|
||||||
- Customizable title and message templates
|
|
||||||
- API key-based authentication
|
|
||||||
- **Ntfy Integration**: Use public ntfy.sh or self-hosted ntfy server
|
|
||||||
- Topic-based routing of notifications
|
|
||||||
- Priority levels for different job events
|
|
||||||
- Optional username/password authentication for private servers
|
|
||||||
- Customizable title and message templates
|
|
||||||
- **Gotify Integration**: Send notifications to self-hosted Gotify servers
|
|
||||||
- Application token-based authentication
|
|
||||||
- Priority levels (1-10) for different notification importance
|
|
||||||
- Customizable title and message templates
|
|
||||||
- **Pushover Integration**: Professional notification delivery service
|
|
||||||
- Application and user key authentication
|
|
||||||
- Device targeting for selective delivery
|
|
||||||
- Sound selection for different notification types
|
|
||||||
- Priority levels from lowest to emergency
|
|
||||||
- Customizable title and message templates
|
|
||||||
- **Common Features**:
|
|
||||||
- Variable substitution in notification templates
|
|
||||||
- Job data access in templates (status, files, bytes, times)
|
|
||||||
- Event-based filtering (job start, completion, errors)
|
|
||||||
- Success/failure tracking for diagnostic purposes
|
|
||||||
|
|
||||||
10. Manage file metadata:
|
|
||||||
- Navigate to the "Files" section to view all processed files
|
|
||||||
- Use filters to quickly find files by status, job ID, or filename
|
|
||||||
- Click on any file to view detailed metadata including timestamps, size, and hash
|
|
||||||
- Use the advanced search page for complex queries with multiple criteria
|
|
||||||
- Delete file metadata records when no longer needed
|
|
||||||
- View files associated with specific jobs by navigating from the job details
|
|
||||||
|
|
||||||
11. Utilize admin tools (administrators only):
|
|
||||||
- Access the "Admin Tools" section from the navigation menu
|
|
||||||
- View system statistics and server information
|
|
||||||
- Create and manage database backups
|
|
||||||
- Browse and download system log files with the integrated log viewer
|
|
||||||
- Perform database maintenance and optimization tasks
|
|
||||||
- View webhook documentation and integration details
|
|
||||||
|
|
||||||
### User Management
|
|
||||||
|
|
||||||
GoMFT uses a role-based access control system with flexible authentication options:
|
|
||||||
|
|
||||||
- **Administrators**: Can create and manage users, access all features
|
|
||||||
- **Regular Users**: Can manage transfers and view history
|
|
||||||
|
|
||||||
#### Authentication Options
|
|
||||||
|
|
||||||
1. **Built-in Authentication**:
|
|
||||||
- Email/password login with secure password hashing
|
|
||||||
- JWT-based session management
|
|
||||||
- Password history tracking
|
|
||||||
- Account lockout protection
|
|
||||||
- Self-service password reset
|
|
||||||
|
|
||||||
2. **External Authentication Providers**:
|
|
||||||
- **Authentik Integration**:
|
|
||||||
- Enterprise-grade SSO capabilities
|
|
||||||
- Automatic user provisioning
|
|
||||||
- Role synchronization
|
|
||||||
- Group mapping support
|
|
||||||
- Secure token exchange
|
|
||||||
|
|
||||||
- **OpenID Connect (OIDC)**:
|
|
||||||
- Standard-compliant identity provider support
|
|
||||||
- Automatic user creation and updates
|
|
||||||
- Role mapping from OIDC claims
|
|
||||||
- Multiple provider support
|
|
||||||
- Secure token validation
|
|
||||||
|
|
||||||
- **OAuth2 Providers**:
|
|
||||||
- Google authentication
|
|
||||||
- GitHub integration
|
|
||||||
- Other OAuth2-compliant providers
|
|
||||||
- Custom provider configuration
|
|
||||||
- Automatic profile synchronization
|
|
||||||
|
|
||||||
3. **Security Features**:
|
|
||||||
- Secure password hashing with bcrypt
|
|
||||||
- JWT-based authentication with tokens
|
|
||||||
- Password history tracking prevents reuse
|
|
||||||
- Account lockout after failed attempts
|
|
||||||
- Two-factor authentication support
|
|
||||||
- Session management and timeout
|
|
||||||
- Secure token storage and handling
|
|
||||||
|
|
||||||
4. **User Profile Management**:
|
|
||||||
- Theme preferences (light/dark mode)
|
|
||||||
- Profile information updates
|
|
||||||
- Password change functionality
|
|
||||||
- Two-factor authentication setup
|
|
||||||
- External account linking
|
|
||||||
|
|
||||||
### Two-Factor Authentication (2FA) Implementation
|
|
||||||
|
|
||||||
#### Overview
|
|
||||||
This implementation adds TOTP-based (Time-based One-Time Password) two-factor authentication support to the application, compatible with standard authenticator apps like Google Authenticator, Authy, and others.
|
|
||||||
|
|
||||||
#### Features
|
|
||||||
- TOTP-based authentication (RFC 6238 compliant)
|
|
||||||
- QR code setup for easy enrollment
|
|
||||||
- Backup codes for account recovery
|
|
||||||
- Rate-limited verification attempts
|
|
||||||
- Secure secret storage
|
|
||||||
|
|
||||||
#### Database Changes
|
|
||||||
The following fields have been added to the `users` table:
|
|
||||||
- `two_factor_secret`: Stores the TOTP secret key
|
|
||||||
- `two_factor_enabled`: Boolean flag indicating if 2FA is enabled
|
|
||||||
- `backup_codes`: Stores recovery backup codes
|
|
||||||
|
|
||||||
#### Setup Process
|
|
||||||
1. Navigate to `/profile/2fa/setup`
|
|
||||||
2. Scan the displayed QR code with your authenticator app
|
|
||||||
3. Enter the verification code to confirm setup
|
|
||||||
4. Save your backup codes in a secure location
|
|
||||||
|
|
||||||
#### Login Flow
|
|
||||||
1. Enter email and password as usual
|
|
||||||
2. If 2FA is enabled:
|
|
||||||
- Enter the 6-digit code from your authenticator app
|
|
||||||
- Alternatively, use a backup code if you can't access your authenticator
|
|
||||||
|
|
||||||
#### Security Considerations
|
|
||||||
- The TOTP secrets are encrypted using AES-256-GCM
|
|
||||||
- You must set the `TOTP_ENCRYPTION_KEY` environment variable in production
|
|
||||||
- This key should be 32 bytes (characters) long and kept confidential
|
|
||||||
- Changing this key after users have set up 2FA will invalidate their existing 2FA configurations
|
|
||||||
- For high-security deployments, store this key in a secure vault and inject it at runtime
|
|
||||||
|
|
||||||
### Transfer Configuration Options
|
|
||||||
|
|
||||||
1. **Source/Destination Types**:
|
|
||||||
- Google Drive
|
|
||||||
- Google Photos
|
|
||||||
- Local filesystem
|
|
||||||
- Amazon S3
|
|
||||||
- MinIO (S3-compatible storage)
|
|
||||||
- NextCloud
|
|
||||||
- Backblaze B2
|
|
||||||
- Wasabi
|
|
||||||
- Hetzner Storage Box
|
|
||||||
- SFTP
|
|
||||||
- FTP
|
|
||||||
- SMB/CIFS shares
|
|
||||||
- And many more via rclone
|
|
||||||
|
|
||||||
2. **Connection Options**:
|
|
||||||
- Host/server addresses
|
|
||||||
- Authentication (username/password or key files)
|
|
||||||
- OAuth2 authentication for Google services
|
|
||||||
- Port configurations
|
|
||||||
- Cloud credentials (access keys, secret keys)
|
|
||||||
- Bucket and region settings
|
|
||||||
- Custom endpoints
|
|
||||||
- Custom rclone flags
|
|
||||||
|
|
||||||
3. **Google Photos Specific Options**:
|
|
||||||
- Read-only mode for safer operations
|
|
||||||
- Start year filter for historical photos
|
|
||||||
- Include/exclude archived media
|
|
||||||
- Album path configuration
|
|
||||||
- Built-in or custom OAuth authentication
|
|
||||||
|
|
||||||
4. **Google Drive Specific Options**:
|
|
||||||
- Folder ID for specific directory access
|
|
||||||
- Team/Shared Drive ID support
|
|
||||||
- Built-in or custom OAuth authentication
|
|
||||||
- Path-based navigation
|
|
||||||
|
|
||||||
5. **File Options**:
|
|
||||||
- File patterns for filtering (e.g., `*.txt`, `data_*.csv`)
|
|
||||||
- Output patterns for dynamic naming
|
|
||||||
- Archive options for transferred files
|
|
||||||
- Skip already processed files to avoid duplicates
|
|
||||||
- Concurrent file transfers (configurable per job)
|
|
||||||
|
|
||||||
6. **Performance Options**:
|
|
||||||
- **Multi-threaded File Transfers**: Process multiple files simultaneously for higher throughput
|
|
||||||
- Configurable concurrency level (1-32 concurrent transfers)
|
|
||||||
- Per-job concurrency settings to optimize for different storage types
|
|
||||||
- Automatic transfer queue management to prevent overloading systems
|
|
||||||
- Adaptive processing based on source/destination capabilities
|
|
||||||
|
|
||||||
7. **Schedule Options**:
|
|
||||||
- Cron expressions for flexible scheduling
|
|
||||||
- Manual execution
|
|
||||||
- Enable/disable schedules
|
|
||||||
|
|
||||||
8. **Notification Options**:
|
|
||||||
- **Email Notifications**: Receive job status updates via email
|
|
||||||
- **Webhook Notifications**: Integration with external systems
|
|
||||||
- **Pushbullet**: Push notifications to your devices
|
|
||||||
- **Ntfy**: Simple push notifications via ntfy.sh
|
|
||||||
- **Gotify**: Self-hosted notification server integration
|
|
||||||
- **Pushover**: Professional notification service
|
|
||||||
- Configure event triggers (start, complete, error)
|
|
||||||
- Customize notification message templates
|
|
||||||
- Selective notification based on job status
|
|
||||||
|
|
||||||
### Email Notifications
|
|
||||||
|
|
||||||
GoMFT supports email notifications for various features:
|
|
||||||
|
|
||||||
- **Password Reset**: Users can request password reset links sent to their registered email
|
|
||||||
- **Styled Emails**: Professional HTML emails that match the application's design theme
|
|
||||||
- **Secure Tokens**: One-time use secure tokens with 15-minute expiration for enhanced security
|
|
||||||
- **Flexible Configuration**: Easily configure your SMTP server settings
|
|
||||||
- **Authentication Options**: Support for both authenticated and unauthenticated SMTP servers
|
|
||||||
- **TLS Support**: Secure communication with your SMTP server
|
|
||||||
- **Development Mode**: When emails are disabled, reset links are logged to the console
|
|
||||||
|
|
||||||
To configure email functionality:
|
|
||||||
|
|
||||||
1. Edit the `.env` file and provide your SMTP server details
|
|
||||||
2. Set `EMAIL_ENABLED=true` in the email configuration section
|
|
||||||
3. Ensure the `BASE_URL` setting is configured correctly for your deployment
|
|
||||||
|
|
||||||
### Webhook Integration
|
|
||||||
|
|
||||||
GoMFT can send webhook notifications to external systems when jobs complete. This allows integration with monitoring tools, chat applications, custom notification systems, or workflow automation platforms.
|
|
||||||
|
|
||||||
#### Webhook Payload Structure
|
|
||||||
|
|
||||||
Webhook notifications are sent as HTTP POST requests with a JSON payload containing detailed information about the job execution:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event_type": "job_execution",
|
|
||||||
"job_id": 123,
|
|
||||||
"job_name": "Daily Backup",
|
|
||||||
"config_id": 456,
|
|
||||||
"config_name": "S3 to Local Backup",
|
|
||||||
"status": "completed",
|
|
||||||
"start_time": "2023-07-14T15:30:00Z",
|
|
||||||
"end_time": "2023-07-14T15:35:42Z",
|
|
||||||
"duration_seconds": 342,
|
|
||||||
"bytes_transferred": 1048576,
|
|
||||||
"files_transferred": 25,
|
|
||||||
"history_id": 789,
|
|
||||||
"source": {
|
|
||||||
"type": "s3",
|
|
||||||
"path": "my-bucket/data"
|
|
||||||
},
|
|
||||||
"destination": {
|
|
||||||
"type": "local",
|
|
||||||
"path": "/backups/data"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For failed transfers, additional error information is included:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "failed",
|
|
||||||
"error_message": "Permission denied accessing destination path"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Webhook Authentication
|
|
||||||
|
|
||||||
When a webhook secret is configured, GoMFT signs the payload using HMAC-SHA256 and includes the signature in the `X-Hub-Signature-256` header. To verify the webhook:
|
|
||||||
|
|
||||||
1. Compute the HMAC-SHA256 of the raw request body using your shared secret
|
|
||||||
2. Compare it with the value in the `X-Hub-Signature-256` header
|
|
||||||
3. Process the webhook only if the signatures match
|
|
||||||
|
|
||||||
This ensures that webhook requests are authentic and haven't been tampered with.
|
|
||||||
|
|
||||||
### Admin Tools
|
|
||||||
|
|
||||||
GoMFT provides a comprehensive set of administrative tools for system management and monitoring:
|
|
||||||
|
|
||||||
#### Log Viewer
|
|
||||||
|
|
||||||
The Admin Tools panel includes an integrated log viewer with the following features:
|
|
||||||
|
|
||||||
- **Log File Browser**: View a list of all available log files in the system
|
|
||||||
- **Real-time Log Viewing**: View log file contents directly in the web interface
|
|
||||||
- **Refresh Function**: Update the log list and content with the latest information
|
|
||||||
- **User-friendly Interface**: Clean, readable presentation with custom scrolling
|
|
||||||
- **Dark Mode Support**: Consistent theming with the rest of the application
|
|
||||||
- **Navigation**: Easily switch between different log files
|
|
||||||
|
|
||||||
This log viewer allows administrators to:
|
|
||||||
- Monitor system activity and diagnose issues without requiring server access
|
|
||||||
- View application logs, scheduler logs, and transfer logs in one place
|
|
||||||
- Track down errors and warning messages in real-time
|
|
||||||
|
|
||||||
#### Database Management
|
|
||||||
|
|
||||||
The Admin Tools interface also includes database management capabilities:
|
|
||||||
- Create and manage database backups
|
|
||||||
- Restore from previous backups
|
|
||||||
- Download backups for safekeeping
|
|
||||||
- View system statistics
|
|
||||||
- Optimize the database with maintenance tools
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── components/ # Templ components for UI
|
|
||||||
├── internal/
|
|
||||||
│ ├── api/ # REST API handlers
|
|
||||||
│ ├── auth/ # Authentication/authorization
|
|
||||||
│ ├── config/ # Configuration management
|
|
||||||
│ ├── db/ # Database models and operations
|
|
||||||
│ ├── email/ # Email service for notifications and password resets
|
|
||||||
│ ├── scheduler/ # Job scheduling and execution
|
|
||||||
│ └── web/ # Web interface handlers
|
|
||||||
├── static/ # Static assets
|
|
||||||
│ ├── css/
|
|
||||||
│ └── js/
|
|
||||||
└── main.go # Application entry point
|
|
||||||
```
|
|
||||||
|
|
||||||
### Technology Stack
|
|
||||||
|
|
||||||
- **Backend**: Go with Gin web framework
|
|
||||||
- **Frontend**: Templ for Go HTML components
|
|
||||||
- **UI Enhancement**: HTMX for dynamic interactions
|
|
||||||
- **Styling**: Tailwind CSS
|
|
||||||
- **Authentication**: JWT (JSON Web Tokens)
|
|
||||||
- **Database**: GORM with SQLite
|
|
||||||
- **File Transfer**: rclone
|
|
||||||
- **Deployment**: Docker containerization and traditional installation
|
|
||||||
|
|
||||||
### Building from Source
|
|
||||||
|
|
||||||
1. Install development dependencies:
|
|
||||||
```bash
|
|
||||||
go install github.com/cosmtrek/air@latest # Hot reload for development
|
|
||||||
go install github.com/a-h/templ/cmd/templ@latest # Templ template compiler
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Generate template code:
|
|
||||||
```bash
|
|
||||||
templ generate
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run in development mode:
|
|
||||||
```bash
|
|
||||||
air
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please see our [Contributing Guide](https://starfleetcptn.github.io/GoMFT/docs/contributing) for details on how to get started.
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch
|
2. Create a feature branch
|
||||||
3. Commit your changes
|
3. Make your changes
|
||||||
4. Push to the branch
|
4. Submit a pull request
|
||||||
5. Create a Pull Request
|
|
||||||
|
|
||||||
---
|
We also welcome documentation improvements. The documentation source is available in the `docs/` directory.
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
GoMFT uses the following directory structure:
|
|
||||||
|
|
||||||
- `/app/data`: Main application data directory
|
|
||||||
- Contains the SQLite database (`gomft.db`)
|
|
||||||
- Contains rclone configurations in `/app/data/configs`
|
|
||||||
- Contains log files in `/app/data/logs`
|
|
||||||
- `/app/backups`: Database backup directory
|
|
||||||
|
|
||||||
When using Docker, you should mount volumes to these locations:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
- /host/path/data:/app/data # For all application data
|
|
||||||
- /host/path/backups:/app/backups # For database backups
|
|
||||||
```
|
|
||||||
|
|
||||||
These paths can be customized using the environment variables `DATA_DIR`, `BACKUP_DIR`, and `LOGS_DIR`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Running as a Non-Root User
|
|
||||||
|
|
||||||
By default, Docker containers run as the root user, which can pose security risks. GoMFT supports running as a non-root user, which is recommended for production environments.
|
|
||||||
|
|
||||||
#### Benefits of Running as Non-Root
|
|
||||||
|
|
||||||
- **Improved Security**: Limits the potential damage if the container is compromised
|
|
||||||
- **Better File Permissions**: Files created by the container will match your host user permissions
|
|
||||||
- **Compliance**: Many security policies and best practices require containers to run as non-root
|
|
||||||
|
|
||||||
#### Methods to Run as Non-Root
|
|
||||||
|
|
||||||
1. **Using PUID/PGID environment variables (recommended)**:
|
|
||||||
```bash
|
|
||||||
# Using current user's ID
|
|
||||||
docker run -e PUID=$(id -u) -e PGID=$(id -g) starfleetcptn/gomft:latest
|
|
||||||
|
|
||||||
# Or in docker-compose.yml
|
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
```
|
|
||||||
This is the most flexible method as it allows changing the user at runtime without rebuilding the image.
|
|
||||||
|
|
||||||
2. **Using the `--user` flag with Docker run**:
|
|
||||||
```bash
|
|
||||||
docker run --user $(id -u):$(id -g) starfleetcptn/gomft:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Using Docker Compose with environment variables for `user` directive**:
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
gomft:
|
|
||||||
image: starfleetcptn/gomft:latest
|
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Building a custom image with specified UID/GID**:
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
gomft:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
UID: ${UID:-1000}
|
|
||||||
GID: ${GID:-1000}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Environment Variables for User Management
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PUID` | User ID to run as | Built-in user ID (1000) |
|
|
||||||
| `PGID` | Group ID to run as | Built-in group ID (1000) |
|
|
||||||
| `USERNAME` | Username to use | `gomft` |
|
|
||||||
|
|
||||||
These environment variables allow you to change the user/group IDs at runtime without rebuilding the image.
|
|
||||||
|
|
||||||
#### Volume Permissions
|
|
||||||
|
|
||||||
When mounting volumes, ensure that the directories on the host have appropriate permissions for the container user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create directories with correct ownership
|
|
||||||
mkdir -p data backups
|
|
||||||
chown -R $(id -u):$(id -g) data backups
|
|
||||||
|
|
||||||
# Or adjust permissions to allow the container user to write
|
|
||||||
mkdir -p data backups
|
|
||||||
chmod -R 777 data backups # Less secure, but easier for testing
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -859,4 +315,4 @@ The GoMFT logo is licensed under the Creative Commons Attribution 4.0 Internatio
|
|||||||
|
|
||||||
The gopher design is from https://github.com/egonelbre/gophers.
|
The gopher design is from https://github.com/egonelbre/gophers.
|
||||||
|
|
||||||
The original Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
The original Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
||||||
@@ -0,0 +1,772 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/config"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/encryption"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/encryption/keyrotation"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create root command
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "gomftctl",
|
||||||
|
Short: "GoMFT Control Tool - Command line utilities for GoMFT",
|
||||||
|
Long: `GoMFT Control Tool (gomftctl) provides command line utilities for managing
|
||||||
|
your GoMFT installation, including database migrations, security key rotation,
|
||||||
|
and other administrative functions.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
rootCmd.AddCommand(createMigrateCmd())
|
||||||
|
rootCmd.AddCommand(createKeyRotationCmd())
|
||||||
|
rootCmd.AddCommand(createVersionCmd())
|
||||||
|
rootCmd.AddCommand(createBackupCmd())
|
||||||
|
rootCmd.AddCommand(createUserCmd())
|
||||||
|
rootCmd.AddCommand(createEncryptionKeyRotationCmd())
|
||||||
|
|
||||||
|
// Execute the root command
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMigrateCmd creates the migrate command for provider data migration
|
||||||
|
func createMigrateCmd() *cobra.Command {
|
||||||
|
var dryRun, validationOnly, force, debugMode, autoFill bool
|
||||||
|
var backupDir string
|
||||||
|
|
||||||
|
migrateCmd := &cobra.Command{
|
||||||
|
Use: "migrate-providers",
|
||||||
|
Short: "Migrate provider data to the new storage provider model",
|
||||||
|
Long: `Migrate provider data extracts unique provider configurations from existing
|
||||||
|
transfer configs and creates dedicated storage provider records.
|
||||||
|
|
||||||
|
This command should be run when upgrading from older versions of GoMFT that
|
||||||
|
stored provider configuration directly in transfer configs.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set backup directory if not provided
|
||||||
|
if backupDir == "" {
|
||||||
|
backupDir = cfg.BackupDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
database, err := db.Initialize(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// Create migration options
|
||||||
|
options := db.MigrateProviderDataOptions{
|
||||||
|
DryRun: dryRun,
|
||||||
|
ValidationOnly: validationOnly,
|
||||||
|
Force: force,
|
||||||
|
BackupDir: backupDir,
|
||||||
|
DebugMode: debugMode,
|
||||||
|
AutoFill: autoFill,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
fmt.Println("Starting provider data migration...")
|
||||||
|
stats, err := database.MigrateProviderData(options)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("\nMigration failed with error:")
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
|
||||||
|
// Add more detailed error information
|
||||||
|
fmt.Println("\nDetailed error information:")
|
||||||
|
fmt.Println("===========================")
|
||||||
|
|
||||||
|
// Unwrap nested errors if possible
|
||||||
|
var currentErr error = err
|
||||||
|
depth := 1
|
||||||
|
for currentErr != nil {
|
||||||
|
fmt.Printf("%d. %v\n", depth, currentErr)
|
||||||
|
if unwrapped, ok := currentErr.(interface{ Unwrap() error }); ok {
|
||||||
|
currentErr = unwrapped.Unwrap()
|
||||||
|
depth++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print database connection information (without sensitive details)
|
||||||
|
fmt.Println("\nDatabase information:")
|
||||||
|
fmt.Printf("- Database path: %s\n", dbPath)
|
||||||
|
fmt.Printf("- Migration options: dryRun=%v, validationOnly=%v, force=%v\n",
|
||||||
|
options.DryRun, options.ValidationOnly, options.Force)
|
||||||
|
|
||||||
|
log.Fatalf("Migration failed. See details above.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print report
|
||||||
|
fmt.Println(db.FormatMigrationReport(stats))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
migrateCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate migration without making changes")
|
||||||
|
migrateCmd.Flags().BoolVar(&validationOnly, "validate-only", false, "Only validate if migration is possible without making changes")
|
||||||
|
migrateCmd.Flags().BoolVar(&force, "force", false, "Force migration even if validation fails")
|
||||||
|
migrateCmd.Flags().StringVar(&backupDir, "backup-dir", "", "Directory to store backup data (defaults to config backup_dir)")
|
||||||
|
migrateCmd.Flags().BoolVar(&debugMode, "debug", false, "Enable debug mode with more detailed error messages")
|
||||||
|
migrateCmd.Flags().BoolVar(&autoFill, "auto-fill", false, "Automatically fill missing required fields with placeholder values")
|
||||||
|
|
||||||
|
return migrateCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createKeyRotationCmd creates the key rotation command
|
||||||
|
func createKeyRotationCmd() *cobra.Command {
|
||||||
|
var keyType string
|
||||||
|
var writeToEnv bool
|
||||||
|
|
||||||
|
keyRotationCmd := &cobra.Command{
|
||||||
|
Use: "rotate-key",
|
||||||
|
Short: "Rotate security keys used by GoMFT",
|
||||||
|
Long: `Rotate security keys generates new cryptographic keys for GoMFT.
|
||||||
|
|
||||||
|
Available key types:
|
||||||
|
- jwt: JSON Web Token signing key
|
||||||
|
- totp: TOTP encryption key
|
||||||
|
- encryption: General encryption key used for sensitive data
|
||||||
|
|
||||||
|
This command will generate a new key and provide instructions for updating
|
||||||
|
your configuration. The application must be restarted for changes to take effect.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Validate key type
|
||||||
|
validTypes := map[string]string{
|
||||||
|
"jwt": "JWT_SECRET",
|
||||||
|
"totp": "TOTP_ENCRYPTION_KEY",
|
||||||
|
"encryption": "GOMFT_ENCRYPTION_KEY",
|
||||||
|
}
|
||||||
|
|
||||||
|
envVar, valid := validTypes[keyType]
|
||||||
|
if !valid {
|
||||||
|
log.Fatalf("Invalid key type: %s. Valid types are: jwt, totp, encryption", keyType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
_, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new key
|
||||||
|
newKey, err := generateSecureKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate secure key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generated new %s key: %s\n\n", keyType, newKey)
|
||||||
|
|
||||||
|
if writeToEnv {
|
||||||
|
// Read current .env file
|
||||||
|
envPath := ".env"
|
||||||
|
envContent, err := os.ReadFile(envPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to read .env file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update .env file with new key
|
||||||
|
updatedEnv, updated := updateEnvVar(string(envContent), envVar, newKey)
|
||||||
|
if !updated {
|
||||||
|
// If the variable wasn't found, append it
|
||||||
|
updatedEnv = updatedEnv + fmt.Sprintf("\n%s=%s\n", envVar, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated content back to .env file
|
||||||
|
if err := os.WriteFile(envPath, []byte(updatedEnv), 0644); err != nil {
|
||||||
|
log.Fatalf("Failed to write updated .env file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updated %s in .env file\n", envVar)
|
||||||
|
fmt.Println("Please restart the GoMFT application for changes to take effect.")
|
||||||
|
} else {
|
||||||
|
// Print instructions for manual update
|
||||||
|
fmt.Println("To use this key, update your .env file with:")
|
||||||
|
fmt.Printf("%s=%s\n\n", envVar, newKey)
|
||||||
|
fmt.Println("Then restart the GoMFT application for changes to take effect.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
keyRotationCmd.Flags().StringVar(&keyType, "type", "", "Type of key to rotate (jwt, totp, encryption)")
|
||||||
|
keyRotationCmd.Flags().BoolVar(&writeToEnv, "write", false, "Write the new key directly to .env file")
|
||||||
|
keyRotationCmd.MarkFlagRequired("type")
|
||||||
|
|
||||||
|
return keyRotationCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createVersionCmd creates the version command
|
||||||
|
func createVersionCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Display version information",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Import the version from the components package
|
||||||
|
fmt.Println("GoMFT Control Tool")
|
||||||
|
fmt.Println("Version: Same as GoMFT application")
|
||||||
|
fmt.Println("Visit https://github.com/starfleetcptn/gomft for more information")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBackupCmd creates the backup command
|
||||||
|
func createBackupCmd() *cobra.Command {
|
||||||
|
var outputDir string
|
||||||
|
|
||||||
|
backupCmd := &cobra.Command{
|
||||||
|
Use: "backup",
|
||||||
|
Short: "Create a backup of the GoMFT database",
|
||||||
|
Long: `Create a backup of the GoMFT database and configuration.
|
||||||
|
The backup includes the SQLite database file and the .env configuration file.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set output directory if not provided
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = cfg.BackupDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
log.Fatalf("Failed to create backup directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create timestamp for backup filename
|
||||||
|
timestamp := fmt.Sprintf("%s", filepath.Base(os.Args[0]))
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
backupPath := filepath.Join(outputDir, fmt.Sprintf("gomft-backup-%s.db", timestamp))
|
||||||
|
|
||||||
|
// Copy database file
|
||||||
|
if err := copyFile(dbPath, backupPath); err != nil {
|
||||||
|
log.Fatalf("Failed to create database backup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy .env file if it exists
|
||||||
|
envPath := ".env"
|
||||||
|
backupEnvPath := filepath.Join(outputDir, fmt.Sprintf("gomft-env-backup-%s.env", timestamp))
|
||||||
|
if _, err := os.Stat(envPath); err == nil {
|
||||||
|
if err := copyFile(envPath, backupEnvPath); err != nil {
|
||||||
|
log.Fatalf("Failed to backup .env file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Configuration backed up to: %s\n", backupEnvPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Database backed up to: %s\n", backupPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
backupCmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to store backup files (defaults to config backup_dir)")
|
||||||
|
|
||||||
|
return backupCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserCmd creates the user management command
|
||||||
|
func createUserCmd() *cobra.Command {
|
||||||
|
userCmd := &cobra.Command{
|
||||||
|
Use: "user",
|
||||||
|
Short: "User management commands",
|
||||||
|
Long: `Commands for managing GoMFT users, including creating, updating, and listing users.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subcommands
|
||||||
|
userCmd.AddCommand(createUserCreateCmd())
|
||||||
|
userCmd.AddCommand(createUserResetPasswordCmd())
|
||||||
|
userCmd.AddCommand(createUserListCmd())
|
||||||
|
|
||||||
|
return userCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserCreateCmd creates the user create command
|
||||||
|
func createUserCreateCmd() *cobra.Command {
|
||||||
|
var email, password string
|
||||||
|
var isAdmin bool
|
||||||
|
|
||||||
|
createCmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new user",
|
||||||
|
Long: `Create a new user with the specified email and password.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
database, err := db.Initialize(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// Create user by first generating password hash
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user object
|
||||||
|
user := &db.User{
|
||||||
|
Email: email,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
LastPasswordChange: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set admin status if requested
|
||||||
|
if isAdmin {
|
||||||
|
user.SetIsAdmin(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user to database
|
||||||
|
if err := database.CreateUser(user); err != nil {
|
||||||
|
log.Fatalf("Failed to create user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("User created successfully:\n")
|
||||||
|
fmt.Printf(" ID: %d\n", user.ID)
|
||||||
|
fmt.Printf(" Email: %s\n", user.Email)
|
||||||
|
fmt.Printf(" Admin: %t\n", user.GetIsAdmin())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
createCmd.Flags().StringVar(&email, "email", "", "User email address")
|
||||||
|
createCmd.Flags().StringVar(&password, "password", "", "User password")
|
||||||
|
createCmd.Flags().BoolVar(&isAdmin, "admin", false, "Grant admin privileges to the user")
|
||||||
|
createCmd.MarkFlagRequired("email")
|
||||||
|
createCmd.MarkFlagRequired("password")
|
||||||
|
|
||||||
|
return createCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserResetPasswordCmd creates the user reset-password command
|
||||||
|
func createUserResetPasswordCmd() *cobra.Command {
|
||||||
|
var email, newPassword string
|
||||||
|
|
||||||
|
resetCmd := &cobra.Command{
|
||||||
|
Use: "reset-password",
|
||||||
|
Short: "Reset a user's password",
|
||||||
|
Long: `Reset the password for a user with the specified email address.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
database, err := db.Initialize(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// Find user by email
|
||||||
|
var user db.User
|
||||||
|
if err := database.Where("email = ?", email).First(&user).Error; err != nil {
|
||||||
|
log.Fatalf("Failed to find user with email %s: %v", email, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new password hash
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user password
|
||||||
|
user.PasswordHash = string(hashedPassword)
|
||||||
|
user.LastPasswordChange = time.Now()
|
||||||
|
|
||||||
|
// Save user to database
|
||||||
|
if err := database.Save(&user).Error; err != nil {
|
||||||
|
log.Fatalf("Failed to update user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Password reset successfully for user: %s\n", email)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
resetCmd.Flags().StringVar(&email, "email", "", "User email address")
|
||||||
|
resetCmd.Flags().StringVar(&newPassword, "password", "", "New password")
|
||||||
|
resetCmd.MarkFlagRequired("email")
|
||||||
|
resetCmd.MarkFlagRequired("password")
|
||||||
|
|
||||||
|
return resetCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserListCmd creates the user list command
|
||||||
|
func createUserListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all users",
|
||||||
|
Long: `List all users in the GoMFT system.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
database, err := db.Initialize(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// Get all users
|
||||||
|
var users []db.User
|
||||||
|
if err := database.Find(&users).Error; err != nil {
|
||||||
|
log.Fatalf("Failed to get users: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print users
|
||||||
|
fmt.Println("GoMFT Users:")
|
||||||
|
fmt.Println("ID\tEmail\tAdmin\tLast Updated")
|
||||||
|
fmt.Println("--------------------------------------------------")
|
||||||
|
for _, user := range users {
|
||||||
|
lastUpdated := "Never"
|
||||||
|
if !user.UpdatedAt.IsZero() {
|
||||||
|
lastUpdated = user.UpdatedAt.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
fmt.Printf("%d\t%s\t%t\t%s\n", user.ID, user.Email, user.GetIsAdmin(), lastUpdated)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createEncryptionKeyRotationCmd creates the encryption key rotation command
|
||||||
|
func createEncryptionKeyRotationCmd() *cobra.Command {
|
||||||
|
var dryRun bool
|
||||||
|
var batchSize, maxErrors int
|
||||||
|
var backupDir string
|
||||||
|
var skipBackup bool
|
||||||
|
var oldKeyEnvVar string
|
||||||
|
var modelsFlag string
|
||||||
|
|
||||||
|
rotateCmd := &cobra.Command{
|
||||||
|
Use: "rotate-encryption-key",
|
||||||
|
Short: "Rotate encryption keys for sensitive data",
|
||||||
|
Long: `Rotate encryption keys for sensitive data stored in the database.
|
||||||
|
|
||||||
|
This command will:
|
||||||
|
1. Create a backup of your database (unless --skip-backup is specified)
|
||||||
|
2. Re-encrypt all sensitive data with a new encryption key
|
||||||
|
3. Provide instructions for updating your configuration
|
||||||
|
|
||||||
|
The application must be stopped before running this command to prevent data corruption.
|
||||||
|
`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set backup directory if not provided
|
||||||
|
if backupDir == "" {
|
||||||
|
backupDir = cfg.BackupDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup if needed
|
||||||
|
if !skipBackup {
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
backupPath := filepath.Join(backupDir, fmt.Sprintf("gomft_backup_before_key_rotation_%s.db",
|
||||||
|
time.Now().Format("20060102_150405")))
|
||||||
|
|
||||||
|
fmt.Printf("Creating database backup at %s...\n", backupPath)
|
||||||
|
if err := copyFile(dbPath, backupPath); err != nil {
|
||||||
|
log.Fatalf("Failed to create backup: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Backup created successfully.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Skipping database backup as requested.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||||
|
database, err := db.Initialize(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
// Setup old encryption service
|
||||||
|
if oldKeyEnvVar == "" {
|
||||||
|
oldKeyEnvVar = encryption.DefaultKeyEnvVar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current encryption service
|
||||||
|
oldService, err := encryption.GetGlobalEncryptionService()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get current encryption service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new key
|
||||||
|
newKey, err := encryption.GenerateKey(encryption.AES256KeySize)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate new encryption key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new key manager for the new key
|
||||||
|
newKeyManager := &keyManager{key: newKey}
|
||||||
|
|
||||||
|
// Setup new encryption service with the new key
|
||||||
|
newService, err := encryption.NewEncryptionService(newKeyManager)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create new encryption service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create rotation options
|
||||||
|
options := keyrotation.RotationOptions{
|
||||||
|
DryRun: dryRun,
|
||||||
|
BatchSize: batchSize,
|
||||||
|
MaxErrors: maxErrors,
|
||||||
|
Timeout: 24 * time.Hour,
|
||||||
|
ProgressCallback: func(modelName string, processed, total int) {
|
||||||
|
fmt.Printf("\rProcessing %s: %d/%d records (%.1f%%)",
|
||||||
|
modelName, processed, total, float64(processed)/float64(total)*100)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create rotation utility
|
||||||
|
rotationUtil, err := keyrotation.NewRotationUtility(
|
||||||
|
database.DB, // Use the underlying gorm.DB
|
||||||
|
oldService,
|
||||||
|
newService,
|
||||||
|
nil, // No auditor needed, keyrotation will use the global one
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create rotation utility: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find models with encrypted fields
|
||||||
|
var models []interface{}
|
||||||
|
if modelsFlag == "auto" {
|
||||||
|
fmt.Println("Automatically detecting models with encrypted fields...")
|
||||||
|
models, err = rotationUtil.FindModelsWithEncryptedFields()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to find models with encrypted fields: %v", err)
|
||||||
|
}
|
||||||
|
if len(models) == 0 {
|
||||||
|
log.Fatalf("No models with encrypted fields found")
|
||||||
|
}
|
||||||
|
} else if modelsFlag != "" {
|
||||||
|
// TODO: Support manual model specification
|
||||||
|
log.Fatalf("Manual model specification not yet implemented, use --models=auto")
|
||||||
|
} else {
|
||||||
|
log.Fatalf("No models specified, use --models=auto to automatically detect models")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create migration plan
|
||||||
|
fmt.Println("Creating encryption migration plan...")
|
||||||
|
plan, err := rotationUtil.CreateEncryptionMigrationPlan(models)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create migration plan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print plan
|
||||||
|
fmt.Println("\nEncryption Migration Plan:")
|
||||||
|
fmt.Printf("Total models: %d\n", len(plan.ModelPlans))
|
||||||
|
fmt.Printf("Total records: %d\n", plan.EstimatedRecords)
|
||||||
|
fmt.Printf("Estimated duration: %s\n", plan.EstimatedDuration.Round(time.Second))
|
||||||
|
fmt.Println("\nModels to process:")
|
||||||
|
for name, modelPlan := range plan.ModelPlans {
|
||||||
|
fmt.Printf("- %s: %d records, %d encrypted fields\n",
|
||||||
|
name, modelPlan.RecordCount, len(modelPlan.EncryptedFields))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm if not in dry run mode
|
||||||
|
if !dryRun {
|
||||||
|
fmt.Println("\nWARNING: This operation will re-encrypt all sensitive data with a new key.")
|
||||||
|
fmt.Println("Make sure the application is stopped before proceeding.")
|
||||||
|
fmt.Print("\nDo you want to continue? [y/N]: ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if strings.ToLower(response) != "y" {
|
||||||
|
fmt.Println("Operation cancelled.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform key rotation
|
||||||
|
fmt.Println("\nStarting key rotation...")
|
||||||
|
startTime := time.Now()
|
||||||
|
stats, err := rotationUtil.RotateKeysForModels(context.Background(), models)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("\nKey rotation failed with error:")
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
|
||||||
|
// Add more detailed error information
|
||||||
|
fmt.Println("\nDetailed error information:")
|
||||||
|
fmt.Println("===========================")
|
||||||
|
|
||||||
|
// Unwrap nested errors if possible
|
||||||
|
var currentErr error = err
|
||||||
|
depth := 1
|
||||||
|
for currentErr != nil {
|
||||||
|
fmt.Printf("%d. %v\n", depth, currentErr)
|
||||||
|
if unwrapped, ok := currentErr.(interface{ Unwrap() error }); ok {
|
||||||
|
currentErr = unwrapped.Unwrap()
|
||||||
|
depth++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print rotation configuration details
|
||||||
|
fmt.Println("\nRotation configuration:")
|
||||||
|
fmt.Printf("- Dry run: %v\n", dryRun)
|
||||||
|
fmt.Printf("- Batch size: %d\n", batchSize)
|
||||||
|
fmt.Printf("- Max errors: %d\n", maxErrors)
|
||||||
|
fmt.Printf("- Models: %s\n", modelsFlag)
|
||||||
|
fmt.Printf("- Old key env var: %s\n", oldKeyEnvVar)
|
||||||
|
|
||||||
|
log.Fatalf("Key rotation failed. See details above.")
|
||||||
|
}
|
||||||
|
duration := time.Since(startTime).Round(time.Second)
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
fmt.Println("\nKey rotation completed successfully!")
|
||||||
|
fmt.Printf("Total records processed: %d/%d\n", stats.ProcessedRecords, stats.TotalRecords)
|
||||||
|
fmt.Printf("Failed records: %d\n", stats.FailedRecords)
|
||||||
|
fmt.Printf("Duration: %s\n", duration)
|
||||||
|
|
||||||
|
if len(stats.Errors) > 0 {
|
||||||
|
fmt.Printf("\nErrors (%d):\n", len(stats.Errors))
|
||||||
|
for i, err := range stats.Errors {
|
||||||
|
if i >= 10 {
|
||||||
|
fmt.Printf("... and %d more errors\n", len(stats.Errors)-10)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("- %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print next steps
|
||||||
|
if !dryRun {
|
||||||
|
fmt.Println("\nNext steps:")
|
||||||
|
fmt.Println("1. Update your environment variable or .env file with the new encryption key:")
|
||||||
|
fmt.Printf(" %s=%s\n", oldKeyEnvVar, base64.StdEncoding.EncodeToString(newKey))
|
||||||
|
fmt.Println("2. Restart your GoMFT application")
|
||||||
|
fmt.Println("\nIMPORTANT: Keep a backup of both the old and new keys until you verify everything works correctly.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nDry run completed. No changes were made to the database.")
|
||||||
|
fmt.Println("Run without --dry-run to perform the actual key rotation.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flags
|
||||||
|
rotateCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate key rotation without making changes")
|
||||||
|
rotateCmd.Flags().IntVar(&batchSize, "batch-size", 100, "Number of records to process in each batch")
|
||||||
|
rotateCmd.Flags().IntVar(&maxErrors, "max-errors", 50, "Maximum number of errors before aborting")
|
||||||
|
rotateCmd.Flags().StringVar(&backupDir, "backup-dir", "", "Directory to store backup data (defaults to config backup_dir)")
|
||||||
|
rotateCmd.Flags().BoolVar(&skipBackup, "skip-backup", false, "Skip database backup (not recommended)")
|
||||||
|
rotateCmd.Flags().StringVar(&oldKeyEnvVar, "old-key-env", "", "Environment variable containing the old encryption key (defaults to GOMFT_ENCRYPTION_KEY)")
|
||||||
|
rotateCmd.Flags().StringVar(&modelsFlag, "models", "auto", "Models to process (use 'auto' for automatic detection)")
|
||||||
|
|
||||||
|
return rotateCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyManager is a simple implementation of the encryption.KeyManager interface
|
||||||
|
// that uses a fixed key for the new encryption service
|
||||||
|
type keyManager struct {
|
||||||
|
key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *keyManager) Initialize() error {
|
||||||
|
// Already initialized with the key
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *keyManager) GetPrimaryKey() ([]byte, error) {
|
||||||
|
return km.key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *keyManager) GetEnvironmentVariableName() string {
|
||||||
|
return "TEMP_KEY_MANAGER"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *keyManager) StoreKeyEnvironment(key []byte) error {
|
||||||
|
// Not needed for this implementation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
// generateSecureKey creates a cryptographically secure random key encoded as base64
|
||||||
|
func generateSecureKey() (string, error) {
|
||||||
|
return config.GenerateSecureKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateEnvVar updates an environment variable in the .env file content
|
||||||
|
func updateEnvVar(content, key, value string) (string, bool) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
prefix := key + "="
|
||||||
|
updated := false
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.HasPrefix(line, prefix) {
|
||||||
|
lines[i] = prefix + value
|
||||||
|
updated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n"), updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies a file from src to dst
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
srcFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
dstFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogEntry represents a log entry for display
|
||||||
|
type LogEntry struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Level string
|
||||||
|
Message string
|
||||||
|
Source string
|
||||||
|
Details map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogViewerData represents the data for the log viewer component
|
||||||
|
type LogViewerData struct {
|
||||||
|
Logs []LogEntry
|
||||||
|
CurrentFilter string
|
||||||
|
LogFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get the appropriate CSS class for log levels
|
||||||
|
func getLogLevelClass(level string) string {
|
||||||
|
baseClass := "px-4 py-2 text-sm font-medium whitespace-nowrap "
|
||||||
|
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
return baseClass + "text-purple-500 dark:text-purple-400"
|
||||||
|
case "info":
|
||||||
|
return baseClass + "text-blue-500 dark:text-blue-400"
|
||||||
|
case "warn":
|
||||||
|
return baseClass + "text-yellow-500 dark:text-yellow-400"
|
||||||
|
case "error":
|
||||||
|
return baseClass + "text-red-500 dark:text-red-400"
|
||||||
|
case "fatal":
|
||||||
|
return baseClass + "text-red-700 dark:text-red-600 font-bold"
|
||||||
|
default:
|
||||||
|
return baseClass + "text-gray-500 dark:text-gray-400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminLogs renders the log viewer page
|
||||||
|
templ AdminLogs(ctx context.Context, data LogViewerData) {
|
||||||
|
@LayoutWithContext("Log Viewer", ctx) {
|
||||||
|
<div class="log-viewer-page">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-stream w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i> Log Viewer
|
||||||
|
</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="pause-logs" class="flex items-center justify-center text-white bg-yellow-500 hover:bg-yellow-600 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg px-4 py-2 dark:bg-yellow-600 dark:hover:bg-yellow-700 focus:outline-none dark:focus:ring-yellow-800">
|
||||||
|
<i class="fas fa-pause w-4 h-4 mr-2"></i> Pause
|
||||||
|
</button>
|
||||||
|
<button id="resume-logs" class="hidden flex items-center justify-center text-white bg-green-500 hover:bg-green-600 focus:ring-4 focus:ring-green-300 font-medium rounded-lg px-4 py-2 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800">
|
||||||
|
<i class="fas fa-play w-4 h-4 mr-2"></i> Resume
|
||||||
|
</button>
|
||||||
|
<button id="clear-logs" class="flex items-center justify-center text-white bg-red-500 hover:bg-red-600 focus:ring-4 focus:ring-red-300 font-medium rounded-lg px-4 py-2 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||||
|
<i class="fas fa-trash w-4 h-4 mr-2"></i> Clear
|
||||||
|
</button>
|
||||||
|
<button id="download-logs" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-download w-4 h-4 mr-2"></i> Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Information -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mb-4 p-4">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<p><i class="fas fa-info-circle mr-2 text-blue-500 dark:text-blue-400"></i> Viewing logs from: <span class="font-mono">{ data.LogFilePath }</span></p>
|
||||||
|
<p><i class="fas fa-circle text-green-500 dark:text-green-400 mr-2"></i> Real-time log streaming is active, logs are automatically captured and displayed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mb-6">
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Filter Logs</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<!-- Log Level Filter -->
|
||||||
|
<div class="flex-1 min-w-[200px]">
|
||||||
|
<label for="filter-level" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Log Level</label>
|
||||||
|
<select id="filter-level" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Levels</option>
|
||||||
|
<option value="debug">Debug</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="warn">Warning</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
<option value="fatal">Fatal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source Filter -->
|
||||||
|
<div class="flex-1 min-w-[200px]">
|
||||||
|
<label for="filter-source" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source</label>
|
||||||
|
<select id="filter-source" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="">All Sources</option>
|
||||||
|
<option value="api">API</option>
|
||||||
|
<option value="web">Web</option>
|
||||||
|
<option value="scheduler">Scheduler</option>
|
||||||
|
<option value="auth">Authentication</option>
|
||||||
|
<option value="database">Database</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Filter -->
|
||||||
|
<div class="flex-1 min-w-[200px]">
|
||||||
|
<label for="filter-search" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Search</label>
|
||||||
|
<input type="text" id="filter-search" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search logs...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Table -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Live Logs</h3>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span id="connection-status" class="flex items-center text-sm text-green-500 dark:text-green-400">
|
||||||
|
<span class="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-2"></span>
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto" style="max-height: 60vh; overflow-y: auto;">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Timestamp</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase w-[100px]">Level</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase w-[120px]">Source</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="log-entries" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<!-- Log entries will be inserted here dynamically -->
|
||||||
|
if len(data.Logs) == 0 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Waiting for logs...</td>
|
||||||
|
</tr>
|
||||||
|
} else {
|
||||||
|
for _, log := range data.Logs {
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{ log.Timestamp.Format("2006-01-02 15:04:05.000") }</td>
|
||||||
|
<td class={ getLogLevelClass(log.Level) }>{ log.Level }</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{ log.Source }</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-900 dark:text-white font-mono">{ log.Message }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const logEntries = document.getElementById('log-entries');
|
||||||
|
const pauseButton = document.getElementById('pause-logs');
|
||||||
|
const resumeButton = document.getElementById('resume-logs');
|
||||||
|
const clearButton = document.getElementById('clear-logs');
|
||||||
|
const downloadButton = document.getElementById('download-logs');
|
||||||
|
const connectionStatus = document.getElementById('connection-status');
|
||||||
|
const filterLevel = document.getElementById('filter-level');
|
||||||
|
const filterSource = document.getElementById('filter-source');
|
||||||
|
const filterSearch = document.getElementById('filter-search');
|
||||||
|
|
||||||
|
let isPaused = false;
|
||||||
|
let logs = [];
|
||||||
|
let filteredLogs = [];
|
||||||
|
let ws;
|
||||||
|
let knownSources = new Set();
|
||||||
|
let reconnectTimer = null;
|
||||||
|
let pingInterval = null;
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
function connectWebSocket() {
|
||||||
|
// Clear any existing reconnect timer
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing ping interval
|
||||||
|
if (pingInterval) {
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
pingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = protocol + '//' + window.location.host + '/admin/logs/ws';
|
||||||
|
|
||||||
|
console.log("Connecting to WebSocket:", wsUrl);
|
||||||
|
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-yellow-500 dark:bg-yellow-400 rounded-full mr-2"></span>Connecting...';
|
||||||
|
connectionStatus.className = 'flex items-center text-sm text-yellow-500 dark:text-yellow-400';
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
console.log("WebSocket connection established");
|
||||||
|
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-2"></span>Connected';
|
||||||
|
connectionStatus.className = 'flex items-center text-sm text-green-500 dark:text-green-400';
|
||||||
|
|
||||||
|
// Set up ping interval to keep connection alive
|
||||||
|
pingInterval = setInterval(function() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
console.debug("Sending ping to server");
|
||||||
|
// Send a simple ping message
|
||||||
|
ws.send(JSON.stringify({type: "ping"}));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 seconds
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
console.log("WebSocket connection closed", event);
|
||||||
|
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Disconnected';
|
||||||
|
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||||
|
|
||||||
|
// Clear the ping interval
|
||||||
|
if (pingInterval) {
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
pingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to reconnect after 5 seconds
|
||||||
|
console.log("Scheduling reconnect in 5 seconds...");
|
||||||
|
reconnectTimer = setTimeout(connectWebSocket, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Error';
|
||||||
|
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||||
|
|
||||||
|
// Don't set up reconnect here, let onclose handle it
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
// Debug log the received data
|
||||||
|
console.debug("Raw log entry received:", event.data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logEntry = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Debug log the parsed entry
|
||||||
|
console.debug("Parsed log entry:", logEntry);
|
||||||
|
|
||||||
|
// Extract source and add to known sources for filtering
|
||||||
|
const source = logEntry.Source || logEntry.source || '';
|
||||||
|
if (source && !knownSources.has(source)) {
|
||||||
|
knownSources.add(source);
|
||||||
|
updateSourceFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle potential log prefixes in the message
|
||||||
|
const message = logEntry.Message || logEntry.message || '';
|
||||||
|
if (message.startsWith("DEBUG:")) {
|
||||||
|
logEntry.Level = "debug";
|
||||||
|
logEntry.Message = message.substring(7).trim();
|
||||||
|
} else if (message.startsWith("INFO:")) {
|
||||||
|
logEntry.Level = "info";
|
||||||
|
logEntry.Message = message.substring(6).trim();
|
||||||
|
} else if (message.startsWith("ERROR:")) {
|
||||||
|
logEntry.Level = "error";
|
||||||
|
logEntry.Message = message.substring(7).trim();
|
||||||
|
} else if (message.startsWith("WARN:")) {
|
||||||
|
logEntry.Level = "warn";
|
||||||
|
logEntry.Message = message.substring(6).trim();
|
||||||
|
} else if (message.startsWith("WARNING:")) {
|
||||||
|
logEntry.Level = "warn";
|
||||||
|
logEntry.Message = message.substring(9).trim();
|
||||||
|
} else if (message.startsWith("FATAL:")) {
|
||||||
|
logEntry.Level = "fatal";
|
||||||
|
logEntry.Message = message.substring(7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to logs array
|
||||||
|
logs.push(logEntry);
|
||||||
|
|
||||||
|
// Apply filters and update display if not paused
|
||||||
|
if (!isPaused) {
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing log entry:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating WebSocket:", error);
|
||||||
|
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Connection Failed';
|
||||||
|
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||||
|
|
||||||
|
// Retry connection after 5 seconds
|
||||||
|
reconnectTimer = setTimeout(connectWebSocket, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the source filter dropdown with dynamically discovered sources
|
||||||
|
function updateSourceFilter() {
|
||||||
|
// Remember the current selection
|
||||||
|
const currentValue = filterSource.value;
|
||||||
|
|
||||||
|
// Clear existing options except the first "All Sources" option
|
||||||
|
while (filterSource.options.length > 1) {
|
||||||
|
filterSource.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sorted sources to dropdown
|
||||||
|
Array.from(knownSources).sort().forEach(source => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = source.toLowerCase();
|
||||||
|
option.textContent = source;
|
||||||
|
filterSource.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore previous selection if it still exists
|
||||||
|
if (currentValue) {
|
||||||
|
for (let i = 0; i < filterSource.options.length; i++) {
|
||||||
|
if (filterSource.options[i].value === currentValue) {
|
||||||
|
filterSource.selectedIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters to logs
|
||||||
|
function applyFilters() {
|
||||||
|
const levelFilter = filterLevel.value.toLowerCase();
|
||||||
|
const sourceFilter = filterSource.value.toLowerCase();
|
||||||
|
const searchFilter = filterSearch.value.toLowerCase();
|
||||||
|
|
||||||
|
filteredLogs = logs.filter(log => {
|
||||||
|
// Handle capitalized properties from the server
|
||||||
|
const level = (log.Level || log.level || '').toLowerCase();
|
||||||
|
const source = (log.Source || log.source || '').toLowerCase();
|
||||||
|
const message = (log.Message || log.message || '').toLowerCase();
|
||||||
|
|
||||||
|
return (levelFilter === '' || level === levelFilter) &&
|
||||||
|
(sourceFilter === '' || source === sourceFilter) &&
|
||||||
|
(searchFilter === '' || message.includes(searchFilter));
|
||||||
|
});
|
||||||
|
|
||||||
|
renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render logs to the table
|
||||||
|
function renderLogs() {
|
||||||
|
// Clear existing logs
|
||||||
|
logEntries.innerHTML = '';
|
||||||
|
|
||||||
|
if (filteredLogs.length === 0) {
|
||||||
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.innerHTML = `<td colspan="4" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">No logs found</td>`;
|
||||||
|
logEntries.appendChild(emptyRow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add filtered logs
|
||||||
|
filteredLogs.forEach(log => {
|
||||||
|
// Handle capitalized property names from the server
|
||||||
|
const timestamp = log.Timestamp || log.timestamp;
|
||||||
|
const level = log.Level || log.level || 'unknown';
|
||||||
|
const source = log.Source || log.source || 'unknown';
|
||||||
|
const message = log.Message || log.message || '';
|
||||||
|
|
||||||
|
let formattedTime;
|
||||||
|
try {
|
||||||
|
// Convert to date object
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
|
// Format in local time with milliseconds
|
||||||
|
const options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format main part of the timestamp
|
||||||
|
formattedTime = date.toLocaleString(undefined, options);
|
||||||
|
|
||||||
|
// Add milliseconds
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
formattedTime += "." + ms;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error formatting timestamp:", e);
|
||||||
|
formattedTime = String(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||||
|
|
||||||
|
let levelClass = 'px-4 py-2 text-sm font-medium whitespace-nowrap ';
|
||||||
|
|
||||||
|
switch(level.toLowerCase()) {
|
||||||
|
case 'debug':
|
||||||
|
levelClass += 'text-purple-500 dark:text-purple-400';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
levelClass += 'text-blue-500 dark:text-blue-400';
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
levelClass += 'text-yellow-500 dark:text-yellow-400';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
levelClass += 'text-red-500 dark:text-red-400';
|
||||||
|
break;
|
||||||
|
case 'fatal':
|
||||||
|
levelClass += 'text-red-700 dark:text-red-600 font-bold';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
levelClass += 'text-gray-500 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML =
|
||||||
|
'<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">' + formattedTime + '</td>' +
|
||||||
|
'<td class="' + levelClass + '">' + level + '</td>' +
|
||||||
|
'<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">' + source + '</td>' +
|
||||||
|
'<td class="px-4 py-2 text-sm text-gray-900 dark:text-white font-mono">' + message + '</td>';
|
||||||
|
|
||||||
|
logEntries.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-scroll to bottom unless user has scrolled up
|
||||||
|
const container = logEntries.parentElement;
|
||||||
|
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 100) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause button click
|
||||||
|
pauseButton.addEventListener('click', function() {
|
||||||
|
isPaused = true;
|
||||||
|
pauseButton.classList.add('hidden');
|
||||||
|
resumeButton.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resume button click
|
||||||
|
resumeButton.addEventListener('click', function() {
|
||||||
|
isPaused = false;
|
||||||
|
resumeButton.classList.add('hidden');
|
||||||
|
pauseButton.classList.remove('hidden');
|
||||||
|
applyFilters(); // Re-apply filters and update
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear button click
|
||||||
|
clearButton.addEventListener('click', function() {
|
||||||
|
logs = [];
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download button click
|
||||||
|
downloadButton.addEventListener('click', function() {
|
||||||
|
// Create CSV from logs
|
||||||
|
let csv = 'Timestamp,Level,Source,Message\n';
|
||||||
|
|
||||||
|
logs.forEach(log => {
|
||||||
|
// Handle capitalized property names from the server
|
||||||
|
const timestamp = log.Timestamp || log.timestamp;
|
||||||
|
const level = log.Level || log.level || 'unknown';
|
||||||
|
const source = log.Source || log.source || 'unknown';
|
||||||
|
const message = log.Message || log.message || '';
|
||||||
|
|
||||||
|
let formattedTime;
|
||||||
|
try {
|
||||||
|
// Convert to date object
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
|
// Format in local time with milliseconds
|
||||||
|
const options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format main part of the timestamp
|
||||||
|
formattedTime = date.toLocaleString(undefined, options);
|
||||||
|
|
||||||
|
// Add milliseconds
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
formattedTime += "." + ms;
|
||||||
|
} catch (e) {
|
||||||
|
formattedTime = String(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properly escape CSV fields
|
||||||
|
let escapedMessage = '';
|
||||||
|
if (message) {
|
||||||
|
escapedMessage = message.split('"').join('""');
|
||||||
|
}
|
||||||
|
|
||||||
|
csv += '"' + formattedTime + '","' + level + '","' + source + '","' + escapedMessage + '"\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and trigger download
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const date = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
||||||
|
|
||||||
|
a.setAttribute('href', url);
|
||||||
|
a.setAttribute('download', 'gomft-logs-' + date + '.csv');
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter change handlers
|
||||||
|
filterLevel.addEventListener('change', applyFilters);
|
||||||
|
filterSource.addEventListener('change', applyFilters);
|
||||||
|
|
||||||
|
// Debounce search input
|
||||||
|
let searchTimeout;
|
||||||
|
filterSearch.addEventListener('input', function() {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(applyFilters, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial connection
|
||||||
|
connectWebSocket();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
"time"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobCalendarData contains the data for the calendar view
|
||||||
|
type JobCalendarData struct {
|
||||||
|
Jobs []db.Job
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCalendarEvents converts jobs to calendar events in JSON format
|
||||||
|
func generateCalendarEvents(jobs []db.Job) string {
|
||||||
|
type CalendarEvent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Start string `json:"start"`
|
||||||
|
End string `json:"end,omitempty"`
|
||||||
|
AllDay bool `json:"allDay,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
ClassName string `json:"className,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
JobID uint `json:"jobId"`
|
||||||
|
JobName string `json:"jobName"`
|
||||||
|
RunTimes []string `json:"runTimes,omitempty"` // Store additional run times for this day
|
||||||
|
RunCount int `json:"runCount,omitempty"` // Count of runs on this day
|
||||||
|
Schedule string `json:"schedule,omitempty"` // Store the schedule for tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []CalendarEvent
|
||||||
|
|
||||||
|
// Set the range for future occurrences - 2 months seems to be a good balance
|
||||||
|
now := time.Now()
|
||||||
|
twoMonthsLater := now.AddDate(0, 2, 0)
|
||||||
|
|
||||||
|
// Map to track events by job ID and date to consolidate multiple occurrences
|
||||||
|
eventsByJobAndDay := make(map[string][]time.Time)
|
||||||
|
|
||||||
|
// Store job information for easy access
|
||||||
|
jobInfo := make(map[uint]struct {
|
||||||
|
Name string
|
||||||
|
Enabled bool
|
||||||
|
Schedule string
|
||||||
|
})
|
||||||
|
|
||||||
|
// First, gather all runs and group them by job ID and day
|
||||||
|
for _, job := range jobs {
|
||||||
|
// Skip jobs with no next run time
|
||||||
|
if job.NextRun == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store job information
|
||||||
|
jobName := job.Name
|
||||||
|
if jobName == "" {
|
||||||
|
jobName = job.Config.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
jobInfo[job.ID] = struct {
|
||||||
|
Name string
|
||||||
|
Enabled bool
|
||||||
|
Schedule string
|
||||||
|
}{
|
||||||
|
Name: jobName,
|
||||||
|
Enabled: job.GetEnabled(),
|
||||||
|
Schedule: job.Schedule,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the first occurrence based on NextRun
|
||||||
|
nextRun := *job.NextRun
|
||||||
|
|
||||||
|
// Skip past events that are more than a day old
|
||||||
|
oneDayAgo := now.AddDate(0, 0, -1)
|
||||||
|
if nextRun.Before(oneDayAgo) {
|
||||||
|
// For past events, if we have LastRun, use that instead
|
||||||
|
if job.LastRun != nil {
|
||||||
|
nextRun = *job.LastRun
|
||||||
|
// Still skip if it's too old
|
||||||
|
if nextRun.Before(oneDayAgo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add initial run to the map
|
||||||
|
dateKey := fmt.Sprintf("%d-%s", job.ID, nextRun.Format("2006-01-02"))
|
||||||
|
eventsByJobAndDay[dateKey] = append(eventsByJobAndDay[dateKey], nextRun)
|
||||||
|
|
||||||
|
// Try to determine future occurrences based on the cron schedule
|
||||||
|
var interval time.Duration
|
||||||
|
schedule := strings.ToLower(job.Schedule)
|
||||||
|
|
||||||
|
// Determine interval based on schedule
|
||||||
|
switch {
|
||||||
|
case strings.Contains(schedule, "every minute") || strings.Contains(schedule, "* * * * *"):
|
||||||
|
interval = 1 * time.Minute
|
||||||
|
case strings.Contains(schedule, "every 5 minutes") || strings.Contains(schedule, "*/5 * * * *"):
|
||||||
|
interval = 5 * time.Minute
|
||||||
|
case strings.Contains(schedule, "every 10 minutes") || strings.Contains(schedule, "*/10 * * * *"):
|
||||||
|
interval = 10 * time.Minute
|
||||||
|
case strings.Contains(schedule, "every 15 minutes") || strings.Contains(schedule, "*/15 * * * *"):
|
||||||
|
interval = 15 * time.Minute
|
||||||
|
case strings.Contains(schedule, "every 30 minutes") || strings.Contains(schedule, "*/30 * * * *"):
|
||||||
|
interval = 30 * time.Minute
|
||||||
|
case strings.Contains(schedule, "hourly") || strings.Contains(schedule, "0 * * * *"):
|
||||||
|
interval = 1 * time.Hour
|
||||||
|
case strings.Contains(schedule, "every 2 hours") || strings.Contains(schedule, "0 */2 * * *"):
|
||||||
|
interval = 2 * time.Hour
|
||||||
|
case strings.Contains(schedule, "every 3 hours") || strings.Contains(schedule, "0 */3 * * *"):
|
||||||
|
interval = 3 * time.Hour
|
||||||
|
case strings.Contains(schedule, "every 4 hours") || strings.Contains(schedule, "0 */4 * * *"):
|
||||||
|
interval = 4 * time.Hour
|
||||||
|
case strings.Contains(schedule, "every 6 hours") || strings.Contains(schedule, "0 */6 * * *"):
|
||||||
|
interval = 6 * time.Hour
|
||||||
|
case strings.Contains(schedule, "every 12 hours") || strings.Contains(schedule, "0 */12 * * *"):
|
||||||
|
interval = 12 * time.Hour
|
||||||
|
case strings.Contains(schedule, "daily") || strings.Contains(schedule, "0 0 * * *"):
|
||||||
|
interval = 24 * time.Hour
|
||||||
|
case strings.Contains(schedule, "weekly") || strings.Contains(schedule, "0 0 * * 0"):
|
||||||
|
interval = 7 * 24 * time.Hour
|
||||||
|
case strings.Contains(schedule, "monthly") || strings.Contains(schedule, "0 0 1 * *"):
|
||||||
|
// Approximate as 30 days
|
||||||
|
interval = 30 * 24 * time.Hour
|
||||||
|
default:
|
||||||
|
// For other schedules, try a simple cron expression check
|
||||||
|
if strings.Contains(schedule, "* * * * *") {
|
||||||
|
// Every minute
|
||||||
|
interval = 1 * time.Minute
|
||||||
|
} else if strings.Contains(schedule, "*/") {
|
||||||
|
// Likely a recurring job with specific interval
|
||||||
|
interval = 1 * time.Hour // Default to hourly as a safe guess
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate future occurrences
|
||||||
|
// Limit the number of runs we'll capture per day
|
||||||
|
maxRunsPerDay := 20
|
||||||
|
|
||||||
|
// Generate future occurrences up to our limits
|
||||||
|
currentTime := nextRun.Add(interval)
|
||||||
|
|
||||||
|
for currentTime.Before(twoMonthsLater) {
|
||||||
|
dateKey := fmt.Sprintf("%d-%s", job.ID, currentTime.Format("2006-01-02"))
|
||||||
|
|
||||||
|
// Check if we already have too many runs for this day
|
||||||
|
if len(eventsByJobAndDay[dateKey]) < maxRunsPerDay {
|
||||||
|
eventsByJobAndDay[dateKey] = append(eventsByJobAndDay[dateKey], currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime = currentTime.Add(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now convert the map to calendar events, consolidating runs on the same day
|
||||||
|
for dateKey, runTimes := range eventsByJobAndDay {
|
||||||
|
// Parse job ID from date key
|
||||||
|
parts := strings.Split(dateKey, "-")
|
||||||
|
jobIDStr := parts[0]
|
||||||
|
|
||||||
|
jobID, _ := strconv.ParseUint(jobIDStr, 10, 32)
|
||||||
|
|
||||||
|
// Get the job info
|
||||||
|
job, exists := jobInfo[uint(jobID)]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort run times chronologically
|
||||||
|
sort.Slice(runTimes, func(i, j int) bool {
|
||||||
|
return runTimes[i].Before(runTimes[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use the first run time as the event time
|
||||||
|
firstRunTime := runTimes[0]
|
||||||
|
|
||||||
|
// Format run times for display in tooltip
|
||||||
|
formattedTimes := make([]string, 0, len(runTimes))
|
||||||
|
for i, rt := range runTimes {
|
||||||
|
// Limit to showing max 10 times in tooltip
|
||||||
|
if i >= 10 {
|
||||||
|
formattedTimes = append(formattedTimes, fmt.Sprintf("... and %d more", len(runTimes)-10))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
formattedTimes = append(formattedTimes, rt.Format("15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set class based on job enabled status
|
||||||
|
className := ""
|
||||||
|
if job.Enabled {
|
||||||
|
className = "bg-blue-200 border-blue-600 text-blue-800 dark:bg-blue-800 dark:border-blue-500 dark:text-blue-100"
|
||||||
|
} else {
|
||||||
|
className = "bg-gray-200 border-gray-400 text-gray-700 dark:bg-gray-700 dark:border-gray-500 dark:text-gray-300"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single event for this job on this day
|
||||||
|
title := job.Name
|
||||||
|
if len(runTimes) > 1 {
|
||||||
|
title = fmt.Sprintf("%s (%d runs)", job.Name, len(runTimes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create event
|
||||||
|
events = append(events, CalendarEvent{
|
||||||
|
ID: fmt.Sprintf("job-%d-%s", jobID, firstRunTime.Format("20060102")),
|
||||||
|
Title: title,
|
||||||
|
Start: firstRunTime.Format(time.RFC3339),
|
||||||
|
AllDay: false,
|
||||||
|
URL: fmt.Sprintf("/jobs/%d", jobID),
|
||||||
|
ClassName: className,
|
||||||
|
Description: fmt.Sprintf("Schedule: %s", job.Schedule),
|
||||||
|
Enabled: job.Enabled,
|
||||||
|
JobID: uint(jobID),
|
||||||
|
JobName: job.Name,
|
||||||
|
RunTimes: formattedTimes,
|
||||||
|
RunCount: len(runTimes),
|
||||||
|
Schedule: job.Schedule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
fmt.Printf("Generated %d consolidated calendar events\n", len(events))
|
||||||
|
|
||||||
|
eventsJSON, err := json.Marshal(events)
|
||||||
|
if err != nil {
|
||||||
|
return "[]" // Return empty array if marshaling fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(eventsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobCalendar displays scheduled jobs in a calendar view
|
||||||
|
templ JobCalendar(ctx context.Context, data JobCalendarData) {
|
||||||
|
@LayoutWithContext("Transfer Calendar", ctx) {
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
|
||||||
|
<div class="flex items-center mb-4 md:mb-0">
|
||||||
|
<i class="fas fa-calendar-alt text-blue-500 mr-2"></i>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Transfer Calendar</h1>
|
||||||
|
</div>
|
||||||
|
<a href="/jobs/new" class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 flex items-center">
|
||||||
|
<i class="fas fa-plus mr-2"></i>
|
||||||
|
New Job
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button id="showAll" class="flex items-center text-sm px-4 py-2 rounded-lg bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-800 dark:text-blue-100 dark:hover:bg-blue-700">
|
||||||
|
<i class="fas fa-calendar-check mr-2"></i>All Jobs
|
||||||
|
</button>
|
||||||
|
<button id="showActive" class="flex items-center text-sm px-4 py-2 rounded-lg bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600">
|
||||||
|
<i class="fas fa-toggle-on mr-2"></i>Active Only
|
||||||
|
</button>
|
||||||
|
<button id="showInactive" class="flex items-center text-sm px-4 py-2 rounded-lg bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600">
|
||||||
|
<i class="fas fa-toggle-off mr-2"></i>Inactive Only
|
||||||
|
</button>
|
||||||
|
<button id="showNext" class="flex items-center text-sm px-4 py-2 rounded-lg bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600">
|
||||||
|
<i class="fas fa-step-forward mr-2"></i>Next Occurrences Only
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden div to store calendar event data -->
|
||||||
|
<div id="calendar-data" style="display: none;">{ generateCalendarEvents(data.Jobs) }</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div id="calendar-loading" class="flex items-center justify-center p-8">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 dark:border-blue-400"></div>
|
||||||
|
<span class="ml-3 text-gray-600 dark:text-gray-400">Loading calendar...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="calendar" class="w-full bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info section -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md mb-8 mt-6 mx-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">About the Calendar View</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
This calendar displays your scheduled transfer jobs for the next 2 months. Click on any event to view or edit the job details.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg mb-4">
|
||||||
|
<p class="text-blue-700 dark:text-blue-300 text-sm">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
Jobs with multiple runs on the same day are consolidated into a single event. Hover over any event to see all scheduled run times for that day.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mt-4 mb-2">Filter Options</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-calendar-check mt-1 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">All Jobs</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Shows all scheduled occurrences in the selected timeframe.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-toggle-on mt-1 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Active Only</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Shows only enabled jobs that will actually run.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-toggle-off mt-1 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Inactive Only</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Shows disabled jobs that won't run unless re-enabled.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i class="fas fa-step-forward mt-1 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Next Occurrences Only</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Shows only the next upcoming occurrence of each job.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mt-4 mb-2">Legend</h3>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 rounded-full bg-blue-500 mr-2"></div>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">Active Jobs</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 rounded-full bg-gray-500 mr-2"></div>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">Inactive Jobs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Local assets are now loaded through vendor.js bundle -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Function to initialize the calendar
|
||||||
|
function initializeCalendar() {
|
||||||
|
// Get the event data from the hidden div
|
||||||
|
try {
|
||||||
|
// Show loading indicator
|
||||||
|
const loadingIndicator = document.getElementById('calendar-loading');
|
||||||
|
const calendarElement = document.getElementById('calendar');
|
||||||
|
|
||||||
|
// Parse the data in a non-blocking way
|
||||||
|
try {
|
||||||
|
const eventsData = JSON.parse(document.getElementById('calendar-data').textContent);
|
||||||
|
|
||||||
|
// Check if FullCalendar is available
|
||||||
|
if (typeof FullCalendar === 'undefined') {
|
||||||
|
console.error('FullCalendar is not loaded yet. Waiting...');
|
||||||
|
setTimeout(initializeCalendar, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize calendar with events data
|
||||||
|
const calendar = new FullCalendar.Calendar(calendarElement, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
plugins: [
|
||||||
|
FullCalendar.dayGridPlugin,
|
||||||
|
FullCalendar.timeGridPlugin,
|
||||||
|
FullCalendar.listPlugin,
|
||||||
|
FullCalendar.interactionPlugin
|
||||||
|
],
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||||
|
},
|
||||||
|
events: eventsData,
|
||||||
|
eventTimeFormat: {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
},
|
||||||
|
eventDidMount: function(info) {
|
||||||
|
// Log the event data for debugging
|
||||||
|
console.log('Event mounted:', info.event.title, 'Enabled:', info.event.extendedProps.enabled);
|
||||||
|
|
||||||
|
// Add tooltips to events
|
||||||
|
if (info.event.extendedProps) {
|
||||||
|
// Only add tooltips to visible events to improve performance
|
||||||
|
if (info.view.type === 'dayGridMonth' &&
|
||||||
|
info.event.start >= calendar.view.activeStart &&
|
||||||
|
info.event.start <= calendar.view.activeEnd) {
|
||||||
|
|
||||||
|
// Create enhanced tooltip content with run times
|
||||||
|
let tooltipContent = `<div class="p-2">`;
|
||||||
|
|
||||||
|
tooltipContent += `<div class="font-bold mb-1">${info.event.title}</div>`;
|
||||||
|
tooltipContent += `<div class="text-sm mb-2">Schedule: ${info.event.extendedProps.schedule || 'Unknown'}</div>`;
|
||||||
|
|
||||||
|
// Show the run times if available
|
||||||
|
if (info.event.extendedProps.runTimes && info.event.extendedProps.runTimes.length > 0) {
|
||||||
|
tooltipContent += `<div class="font-bold text-xs mt-1">Run Times:</div>`;
|
||||||
|
tooltipContent += `<div class="text-xs">`;
|
||||||
|
|
||||||
|
// Show the run times in a list
|
||||||
|
info.event.extendedProps.runTimes.forEach(time => {
|
||||||
|
tooltipContent += `<div>${time}</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipContent += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipContent += `<div class="text-xs mt-2">Click to view/edit job</div>`;
|
||||||
|
tooltipContent += `</div>`;
|
||||||
|
|
||||||
|
tippy(info.el, {
|
||||||
|
content: tooltipContent,
|
||||||
|
allowHTML: true,
|
||||||
|
placement: 'top',
|
||||||
|
arrow: true,
|
||||||
|
interactive: true,
|
||||||
|
maxWidth: 300,
|
||||||
|
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventClick: function(info) {
|
||||||
|
// Use the URL property to navigate to job details
|
||||||
|
if (info.event.url) {
|
||||||
|
window.location.href = info.event.url;
|
||||||
|
return false; // Prevents the default action
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventWillUnmount: function(info) {
|
||||||
|
// Cleanup any tooltips to prevent memory leaks
|
||||||
|
if (info.el._tippy) {
|
||||||
|
info.el._tippy.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
themeSystem: 'standard',
|
||||||
|
loading: function(isLoading) {
|
||||||
|
if (!isLoading) {
|
||||||
|
// Hide loading indicator and show calendar
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
|
calendarElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
// Handle filter buttons with optimized filtering
|
||||||
|
document.getElementById('showAll').addEventListener('click', function() {
|
||||||
|
updateActiveButton(this);
|
||||||
|
// Show all events
|
||||||
|
calendar.getEvents().forEach(event => {
|
||||||
|
event.setProp('display', '');
|
||||||
|
});
|
||||||
|
calendar.render(); // Re-render to apply changes
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('showActive').addEventListener('click', function() {
|
||||||
|
updateActiveButton(this);
|
||||||
|
// Show only active events
|
||||||
|
calendar.getEvents().forEach(event => {
|
||||||
|
const isEnabled = event.extendedProps.enabled;
|
||||||
|
event.setProp('display', isEnabled ? '' : 'none');
|
||||||
|
});
|
||||||
|
calendar.render(); // Re-render to apply changes
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('showInactive').addEventListener('click', function() {
|
||||||
|
updateActiveButton(this);
|
||||||
|
// Show only inactive events
|
||||||
|
calendar.getEvents().forEach(event => {
|
||||||
|
const isEnabled = event.extendedProps.enabled;
|
||||||
|
event.setProp('display', !isEnabled ? '' : 'none');
|
||||||
|
});
|
||||||
|
calendar.render(); // Re-render to apply changes
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('showNext').addEventListener('click', function() {
|
||||||
|
updateActiveButton(this);
|
||||||
|
|
||||||
|
console.log("Next Occurrences Only filter clicked");
|
||||||
|
|
||||||
|
// First reset all events to make sure none are hidden
|
||||||
|
calendar.getEvents().forEach(event => {
|
||||||
|
event.setProp('display', 'none');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get a map of all jobs
|
||||||
|
const jobIDs = new Set();
|
||||||
|
calendar.getEvents().forEach(event => {
|
||||||
|
if (event.extendedProps && event.extendedProps.jobId) {
|
||||||
|
jobIDs.add(event.extendedProps.jobId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Found jobs:", Array.from(jobIDs));
|
||||||
|
|
||||||
|
// For each job, find the next occurrence and show it
|
||||||
|
const now = new Date();
|
||||||
|
jobIDs.forEach(jobId => {
|
||||||
|
// Get all events for this job
|
||||||
|
const jobEvents = calendar.getEvents().filter(event =>
|
||||||
|
event.extendedProps &&
|
||||||
|
event.extendedProps.jobId === jobId
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Job ${jobId}: Found ${jobEvents.length} events`);
|
||||||
|
|
||||||
|
// Find future events
|
||||||
|
const futureEvents = jobEvents.filter(event =>
|
||||||
|
new Date(event.start) >= now
|
||||||
|
).sort((a, b) =>
|
||||||
|
new Date(a.start) - new Date(b.start)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we have future events, show the earliest one
|
||||||
|
if (futureEvents.length > 0) {
|
||||||
|
console.log(`Job ${jobId}: Next occurrence at ${futureEvents[0].start}`);
|
||||||
|
futureEvents[0].setProp('display', '');
|
||||||
|
} else {
|
||||||
|
// If no future events, find most recent past event
|
||||||
|
const pastEvents = jobEvents.filter(event =>
|
||||||
|
new Date(event.start) < now
|
||||||
|
).sort((a, b) =>
|
||||||
|
new Date(b.start) - new Date(a.start)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pastEvents.length > 0) {
|
||||||
|
console.log(`Job ${jobId}: Most recent occurrence at ${pastEvents[0].start}`);
|
||||||
|
pastEvents[0].setProp('display', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render(); // Re-render to apply changes
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateActiveButton(activeBtn) {
|
||||||
|
// Reset all buttons
|
||||||
|
document.querySelectorAll('#showAll, #showActive, #showInactive, #showNext').forEach(btn => {
|
||||||
|
btn.classList.remove('bg-blue-100', 'text-blue-800', 'dark:bg-blue-800', 'dark:text-blue-100');
|
||||||
|
btn.classList.add('bg-gray-100', 'text-gray-800', 'dark:bg-gray-700', 'dark:text-gray-100');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set active button
|
||||||
|
activeBtn.classList.remove('bg-gray-100', 'text-gray-800', 'dark:bg-gray-700', 'dark:text-gray-100');
|
||||||
|
activeBtn.classList.add('bg-blue-100', 'text-blue-800', 'dark:bg-blue-800', 'dark:text-blue-100');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle theme changes
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
// Update tooltips theme
|
||||||
|
document.querySelectorAll('[data-tippy-root]').forEach(tooltip => {
|
||||||
|
tooltip.className = document.documentElement.classList.contains('dark')
|
||||||
|
? 'tippy-box dark-theme'
|
||||||
|
: 'tippy-box light-theme';
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing calendar:", error);
|
||||||
|
loadingIndicator.innerHTML = '<div class="text-red-500"><i class="fas fa-exclamation-triangle mr-2"></i>Error loading calendar data</div>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during initial calendar setup:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for vendor.js to load before initializing calendar
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
setTimeout(initializeCalendar, 100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fc-event {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style consolidated events */
|
||||||
|
.fc-event-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip styles */
|
||||||
|
.tippy-box {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~='dark'] {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~='light'] {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #1f2937;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.fc {
|
||||||
|
--fc-page-bg-color: #1f2937;
|
||||||
|
--fc-border-color: #374151;
|
||||||
|
--fc-neutral-bg-color: #374151;
|
||||||
|
--fc-neutral-text-color: #e5e7eb;
|
||||||
|
--fc-today-bg-color: rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: rgba(59, 130, 246, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-col-header-cell {
|
||||||
|
background-color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-scrollgrid-sync-inner {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ type ConfigFormData struct {
|
|||||||
InitialCommand *db.RcloneCommand
|
InitialCommand *db.RcloneCommand
|
||||||
SelectedFlagsMap map[uint]bool
|
SelectedFlagsMap map[uint]bool
|
||||||
SelectedFlagValues map[uint]string
|
SelectedFlagValues map[uint]string
|
||||||
|
// Add source and destination providers
|
||||||
|
SourceProviders []db.StorageProvider
|
||||||
|
DestinationProviders []db.StorageProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigFormTitle(isNew bool) string {
|
func getConfigFormTitle(isNew bool) string {
|
||||||
@@ -92,6 +95,13 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
useBuiltinAuthSource := true
|
useBuiltinAuthSource := true
|
||||||
useBuiltinAuthDest := true
|
useBuiltinAuthDest := true
|
||||||
|
|
||||||
|
// Provider configuration
|
||||||
|
useSourceProvider := false
|
||||||
|
sourceProviderId := uint(0)
|
||||||
|
|
||||||
|
useDestinationProvider := false
|
||||||
|
destinationProviderId := uint(0)
|
||||||
|
|
||||||
// If editing an existing config, populate with those values
|
// If editing an existing config, populate with those values
|
||||||
if config != nil {
|
if config != nil {
|
||||||
name = config.Name
|
name = config.Name
|
||||||
@@ -167,6 +177,9 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
deleteAfterTransfer = config.GetDeleteAfterTransfer()
|
deleteAfterTransfer = config.GetDeleteAfterTransfer()
|
||||||
skipProcessedFiles = config.GetSkipProcessedFiles()
|
skipProcessedFiles = config.GetSkipProcessedFiles()
|
||||||
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
||||||
|
if maxConcurrentTransfers <= 0 {
|
||||||
|
maxConcurrentTransfers = 1 // Ensure at least 1 concurrent transfer
|
||||||
|
}
|
||||||
rcloneFlags = config.RcloneFlags
|
rcloneFlags = config.RcloneFlags
|
||||||
commandId = config.CommandID
|
commandId = config.CommandID
|
||||||
commandFlags = config.CommandFlags
|
commandFlags = config.CommandFlags
|
||||||
@@ -180,6 +193,17 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
} else if destClientId != "" || destClientSecret != "" {
|
} else if destClientId != "" || destClientSecret != "" {
|
||||||
useBuiltinAuthDest = false
|
useBuiltinAuthDest = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider reference fields
|
||||||
|
if config.IsUsingSourceProviderReference() {
|
||||||
|
useSourceProvider = true
|
||||||
|
sourceProviderId = *config.SourceProviderID
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IsUsingDestinationProviderReference() {
|
||||||
|
useDestinationProvider = true
|
||||||
|
destinationProviderId = *config.DestinationProviderID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the JSON-formatted string with all the data, add new path validation states
|
// Return the JSON-formatted string with all the data, add new path validation states
|
||||||
@@ -209,6 +233,9 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
sourceStartYear: %d,
|
sourceStartYear: %d,
|
||||||
sourceIncludeArchived: %v,
|
sourceIncludeArchived: %v,
|
||||||
|
|
||||||
|
useSourceProvider: %v,
|
||||||
|
sourceProviderId: %d,
|
||||||
|
|
||||||
filePattern: '%s',
|
filePattern: '%s',
|
||||||
outputPattern: '%s',
|
outputPattern: '%s',
|
||||||
|
|
||||||
@@ -236,6 +263,9 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destStartYear: %d,
|
destStartYear: %d,
|
||||||
destIncludeArchived: %v,
|
destIncludeArchived: %v,
|
||||||
|
|
||||||
|
useDestinationProvider: %v,
|
||||||
|
destinationProviderId: %d,
|
||||||
|
|
||||||
useBuiltinAuthSource: %v,
|
useBuiltinAuthSource: %v,
|
||||||
useBuiltinAuthDest: %v,
|
useBuiltinAuthDest: %v,
|
||||||
|
|
||||||
@@ -344,11 +374,13 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
sourceBucket, sourceRegion, sourceAccessKey, sourceSecretKey, sourceEndpoint, sourceShare, sourceDomain, sourcePassiveMode,
|
sourceBucket, sourceRegion, sourceAccessKey, sourceSecretKey, sourceEndpoint, sourceShare, sourceDomain, sourcePassiveMode,
|
||||||
sourceClientId, sourceClientSecret, sourceDriveId, sourceTeamDrive,
|
sourceClientId, sourceClientSecret, sourceDriveId, sourceTeamDrive,
|
||||||
sourceReadOnly, sourceStartYear, sourceIncludeArchived,
|
sourceReadOnly, sourceStartYear, sourceIncludeArchived,
|
||||||
|
useSourceProvider, sourceProviderId,
|
||||||
filePattern, outputPattern,
|
filePattern, outputPattern,
|
||||||
destinationType, destinationPath, destHost, destPort, destUser, destPassword, destKeyFile, destAuthType,
|
destinationType, destinationPath, destHost, destPort, destUser, destPassword, destKeyFile, destAuthType,
|
||||||
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint, destShare, destDomain, destPassiveMode,
|
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint, destShare, destDomain, destPassiveMode,
|
||||||
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
||||||
destReadOnly, destStartYear, destIncludeArchived,
|
destReadOnly, destStartYear, destIncludeArchived,
|
||||||
|
useDestinationProvider, destinationProviderId,
|
||||||
useBuiltinAuthSource, useBuiltinAuthDest,
|
useBuiltinAuthSource, useBuiltinAuthDest,
|
||||||
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags,
|
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags,
|
||||||
commandId, commandFlags)
|
commandId, commandFlags)
|
||||||
@@ -377,6 +409,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
|
|
||||||
<!-- Main Form -->
|
<!-- Main Form -->
|
||||||
<form
|
<form
|
||||||
|
id="config-form"
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
if data.IsNew {
|
if data.IsNew {
|
||||||
hx-post="/configs"
|
hx-post="/configs"
|
||||||
@@ -413,8 +446,40 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure maxConcurrentTransfers is at least 1
|
||||||
|
if (!maxConcurrentTransfers || maxConcurrentTransfers < 1) {
|
||||||
|
maxConcurrentTransfers = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize provider selection states
|
||||||
|
useSourceProvider = useSourceProvider || false;
|
||||||
|
useDestinationProvider = useDestinationProvider || false;
|
||||||
|
|
||||||
// Initialize command requirements
|
// Initialize command requirements
|
||||||
updateCommandRequirements();
|
updateCommandRequirements();
|
||||||
|
|
||||||
|
// Initialize hidden input fields with their values to ensure they're included in form submission
|
||||||
|
document.getElementById('hidden_name').value = name;
|
||||||
|
document.getElementById('hidden_source_path').value = sourcePath;
|
||||||
|
document.getElementById('hidden_destination_path').value = destinationPath;
|
||||||
|
|
||||||
|
// Initialize S3 source fields if applicable
|
||||||
|
if (sourceType === 's3' || sourceType === 'b2' || sourceType === 'wasabi' || sourceType === 'minio') {
|
||||||
|
document.getElementById('hidden_source_access_key').value = sourceAccessKey;
|
||||||
|
document.getElementById('hidden_source_secret_key').value = sourceSecretKey;
|
||||||
|
document.getElementById('hidden_source_endpoint').value = sourceEndpoint;
|
||||||
|
document.getElementById('hidden_source_bucket').value = sourceBucket;
|
||||||
|
document.getElementById('hidden_source_region').value = sourceRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize S3 destination fields if applicable
|
||||||
|
if (destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio') {
|
||||||
|
document.getElementById('hidden_dest_access_key').value = destAccessKey;
|
||||||
|
document.getElementById('hidden_dest_secret_key').value = destSecretKey;
|
||||||
|
document.getElementById('hidden_dest_endpoint').value = destEndpoint;
|
||||||
|
document.getElementById('hidden_dest_bucket').value = destBucket;
|
||||||
|
document.getElementById('hidden_dest_region').value = destRegion;
|
||||||
|
}
|
||||||
})"
|
})"
|
||||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
||||||
sourcePort = 22;
|
sourcePort = 22;
|
||||||
@@ -435,8 +500,60 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
destPort = 23;
|
destPort = 23;
|
||||||
console.log('Updating destination port to 23 for Hetzner');
|
console.log('Updating destination port to 23 for Hetzner');
|
||||||
}"
|
}"
|
||||||
|
@formvalidation
|
||||||
|
@submit="
|
||||||
|
// Basic fields
|
||||||
|
document.getElementById('hidden_name').value = name;
|
||||||
|
document.getElementById('hidden_source_path').value = sourcePath;
|
||||||
|
document.getElementById('hidden_destination_path').value = destinationPath;
|
||||||
|
|
||||||
|
// S3 source fields
|
||||||
|
if (sourceType === 's3' || sourceType === 'b2' || sourceType === 'wasabi' || sourceType === 'minio') {
|
||||||
|
document.getElementById('hidden_source_access_key').value = sourceAccessKey;
|
||||||
|
document.getElementById('hidden_source_secret_key').value = sourceSecretKey;
|
||||||
|
document.getElementById('hidden_source_endpoint').value = sourceEndpoint;
|
||||||
|
document.getElementById('hidden_source_bucket').value = sourceBucket;
|
||||||
|
document.getElementById('hidden_source_region').value = sourceRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3 destination fields
|
||||||
|
if (destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio') {
|
||||||
|
document.getElementById('hidden_dest_access_key').value = destAccessKey;
|
||||||
|
document.getElementById('hidden_dest_secret_key').value = destSecretKey;
|
||||||
|
document.getElementById('hidden_dest_endpoint').value = destEndpoint;
|
||||||
|
document.getElementById('hidden_dest_bucket').value = destBucket;
|
||||||
|
document.getElementById('hidden_dest_region').value = destRegion;
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- Hidden fields to ensure values are submitted with the form -->
|
||||||
|
<input type="hidden" id="hidden_name" name="name" />
|
||||||
|
<input type="hidden" id="hidden_source_path" name="source_path" />
|
||||||
|
<input type="hidden" id="hidden_destination_path" name="destination_path" />
|
||||||
|
|
||||||
|
<!-- Hidden fields for S3-compatible providers -->
|
||||||
|
<input type="hidden" id="hidden_source_access_key" name="source_access_key" />
|
||||||
|
<input type="hidden" id="hidden_source_secret_key" name="source_secret_key" />
|
||||||
|
<input type="hidden" id="hidden_source_endpoint" name="source_endpoint" />
|
||||||
|
<input type="hidden" id="hidden_source_bucket" name="source_bucket" />
|
||||||
|
<input type="hidden" id="hidden_source_region" name="source_region" />
|
||||||
|
|
||||||
|
<input type="hidden" id="hidden_dest_access_key" name="dest_access_key" />
|
||||||
|
<input type="hidden" id="hidden_dest_secret_key" name="dest_secret_key" />
|
||||||
|
<input type="hidden" id="hidden_dest_endpoint" name="dest_endpoint" />
|
||||||
|
<input type="hidden" id="hidden_dest_bucket" name="dest_bucket" />
|
||||||
|
<input type="hidden" id="hidden_dest_region" name="dest_region" />
|
||||||
|
|
||||||
|
<!-- Form Error Container -->
|
||||||
|
<div id="form-errors" class="hidden p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800/20 dark:text-red-400 border border-red-200 dark:border-red-900" role="alert">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-500 mr-2"></i>
|
||||||
|
<h3 class="text-base font-medium text-red-800 dark:text-red-400">Please correct the following errors:</h3>
|
||||||
|
</div>
|
||||||
|
<ul id="error-list" class="ml-5 list-disc space-y-1"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Details Section -->
|
<!-- Configuration Details Section -->
|
||||||
<div class="p-4 mb-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-900">
|
<div class="p-4 mb-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-900">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
@@ -450,13 +567,17 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
|
|
||||||
<!-- Name field -->
|
<!-- Name field -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Configuration Name</label>
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Configuration Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
x-model="name"
|
x-model="name"
|
||||||
|
@input="document.getElementById('hidden_name').value = name"
|
||||||
required
|
required
|
||||||
|
aria-required="true"
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="My Transfer Configuration"
|
placeholder="My Transfer Configuration"
|
||||||
/>
|
/>
|
||||||
@@ -490,8 +611,9 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Source selection -->
|
<!-- Source selection -->
|
||||||
@common.SourceSelection()
|
@common.SourceSelection(data.SourceProviders)
|
||||||
|
|
||||||
|
<!-- Test Source Connection button -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
@@ -503,60 +625,103 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<i class="fas fa-plug mr-1"></i> Test Source
|
<i class="fas fa-plug mr-1"></i> Test Source
|
||||||
<span id="source-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
<span id="source-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<!-- Removed target div, result shown via toast -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Source type specific forms -->
|
<!-- Source type specific forms - only show when not using a provider -->
|
||||||
<template x-if="sourceType === 'local'">
|
<template x-if="!useSourceProvider">
|
||||||
@source.LocalSourceForm()
|
<div class="mt-4">
|
||||||
|
<template x-if="sourceType === 'local'">
|
||||||
|
@source.LocalSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'sftp'">
|
||||||
|
@source.SFTPSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'ftp'">
|
||||||
|
@source.FTPSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 's3'">
|
||||||
|
@source.S3SourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'b2'">
|
||||||
|
@source.B2SourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'wasabi'">
|
||||||
|
@source.WasabiSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'minio'">
|
||||||
|
@source.MinIOSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'smb'">
|
||||||
|
@source.SMBSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'webdav'">
|
||||||
|
@source.WebDAVSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'nextcloud'">
|
||||||
|
@source.NextCloudSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'gdrive'">
|
||||||
|
@source.GoogleDriveSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'gphotos'">
|
||||||
|
@source.GooglePhotosSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'hetzner'">
|
||||||
|
@source.HetznerSourceForm()
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'sftp'">
|
<!-- Provider usage notice -->
|
||||||
@source.SFTPSourceForm()
|
<template x-if="useSourceProvider">
|
||||||
</template>
|
<div class="mt-4 p-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-800">
|
||||||
|
<div class="flex">
|
||||||
<template x-if="sourceType === 'ftp'">
|
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mt-0.5 mr-2"></i>
|
||||||
@source.FTPSourceForm()
|
<div>
|
||||||
</template>
|
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
Using storage provider configuration. Customize source path options below if needed.
|
||||||
<template x-if="sourceType === 's3'">
|
</p>
|
||||||
@source.S3SourceForm()
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</div>
|
||||||
<template x-if="sourceType === 'b2'">
|
|
||||||
@source.B2SourceForm()
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'wasabi'">
|
<!-- Always show source path field with provider or without provider -->
|
||||||
@source.WasabiSourceForm()
|
<template x-if="useSourceProvider && sourceProviderId > 0">
|
||||||
</template>
|
<div class="mt-4">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source Path</label>
|
||||||
<template x-if="sourceType === 'minio'">
|
<div class="relative">
|
||||||
@source.MinIOSourceForm()
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
</template>
|
<i class="fas fa-folder text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
<template x-if="sourceType === 'smb'">
|
<input
|
||||||
@source.SMBSourceForm()
|
type="text"
|
||||||
</template>
|
id="source_path"
|
||||||
|
name="source_path"
|
||||||
<template x-if="sourceType === 'webdav'">
|
x-model="sourcePath"
|
||||||
@source.WebDAVSourceForm()
|
@input="document.getElementById('hidden_source_path').value = sourcePath"
|
||||||
</template>
|
@blur="checkPath(sourcePath, 'source')"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<template x-if="sourceType === 'nextcloud'">
|
placeholder="/path/to/source"
|
||||||
@source.NextCloudSourceForm()
|
/>
|
||||||
</template>
|
<template x-if="sourcePathValid === false">
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-500" x-text="sourcePathError"></p>
|
||||||
<template x-if="sourceType === 'gdrive'">
|
</template>
|
||||||
@source.GoogleDriveSourceForm()
|
</div>
|
||||||
</template>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Specify source path within the provider</p>
|
||||||
|
</div>
|
||||||
<template x-if="sourceType === 'gphotos'">
|
|
||||||
@source.GooglePhotosSourceForm()
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="sourceType === 'hetzner'">
|
|
||||||
@source.HetznerSourceForm()
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -570,15 +735,16 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
@common.FilePatternFields()
|
@common.FilePatternFields()
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Destination Configuration Section (only shown if required) -->
|
<!-- Destination Configuration Section -->
|
||||||
<div x-show="requiresDestination" x-transition class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
<div x-show="requiresDestination" x-transition class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||||
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
<i class="fas fa-download mr-2 text-blue-500 dark:text-blue-400"></i>Destination Configuration
|
<i class="fas fa-download mr-2 text-blue-500 dark:text-blue-400"></i>Destination Configuration
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Destination selection -->
|
<!-- Destination selection -->
|
||||||
@common.DestinationSelection()
|
@common.DestinationSelection(data.DestinationProviders)
|
||||||
|
|
||||||
|
<!-- Test Destination button -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
@@ -590,60 +756,103 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<i class="fas fa-plug mr-1"></i> Test Destination
|
<i class="fas fa-plug mr-1"></i> Test Destination
|
||||||
<span id="dest-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
<span id="dest-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<!-- Removed target div, result shown via toast -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Destination type specific forms -->
|
<!-- Destination type specific forms - only show when not using a provider -->
|
||||||
<template x-if="destinationType === 'local'">
|
<template x-if="!useDestinationProvider">
|
||||||
@destination.LocalDestinationForm()
|
<div class="mt-4">
|
||||||
</template>
|
<template x-if="destinationType === 'local'">
|
||||||
|
@destination.LocalDestinationForm()
|
||||||
<template x-if="destinationType === 'sftp'">
|
</template>
|
||||||
@destination.SFTPDestinationForm()
|
|
||||||
</template>
|
<template x-if="destinationType === 'sftp'">
|
||||||
|
@destination.SFTPDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'ftp'">
|
<template x-if="destinationType === 'ftp'">
|
||||||
@destination.FTPDestinationForm()
|
@destination.FTPDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 's3'">
|
<template x-if="destinationType === 's3'">
|
||||||
@destination.S3DestinationForm()
|
@destination.S3DestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'b2'">
|
<template x-if="destinationType === 'b2'">
|
||||||
@destination.B2DestinationForm()
|
@destination.B2DestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'wasabi'">
|
<template x-if="destinationType === 'wasabi'">
|
||||||
@destination.WasabiDestinationForm()
|
@destination.WasabiDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'minio'">
|
<template x-if="destinationType === 'minio'">
|
||||||
@destination.MinIODestinationForm()
|
@destination.MinIODestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'smb'">
|
<template x-if="destinationType === 'smb'">
|
||||||
@destination.SMBDestinationForm()
|
@destination.SMBDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'nextcloud'">
|
<template x-if="destinationType === 'nextcloud'">
|
||||||
@destination.NextCloudDestinationForm()
|
@destination.NextCloudDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'webdav'">
|
<template x-if="destinationType === 'webdav'">
|
||||||
@destination.WebDAVDestinationForm()
|
@destination.WebDAVDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'gdrive'">
|
<template x-if="destinationType === 'gdrive'">
|
||||||
@destination.GoogleDriveDestinationForm()
|
@destination.GoogleDriveDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'gphotos'">
|
||||||
|
@destination.GooglePhotosDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'hetzner'">
|
||||||
|
@destination.HetznerDestinationForm()
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'gphotos'">
|
<!-- Provider usage notice -->
|
||||||
@destination.GooglePhotosDestinationForm()
|
<template x-if="useDestinationProvider">
|
||||||
|
<div class="mt-4 p-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-800">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mt-0.5 mr-2"></i>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
Using storage provider configuration. Customize destination path options below if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'hetzner'">
|
<!-- Always show destination path field with provider or without provider -->
|
||||||
@destination.HetznerDestinationForm()
|
<template x-if="useDestinationProvider && destinationProviderId > 0">
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Destination Path</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-folder text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="destination_path"
|
||||||
|
name="destination_path"
|
||||||
|
x-model="destinationPath"
|
||||||
|
@input="document.getElementById('hidden_destination_path').value = destinationPath"
|
||||||
|
@blur="checkPath(destinationPath, 'dest')"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="/path/to/destination"
|
||||||
|
/>
|
||||||
|
<template x-if="destPathValid === false">
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-500" x-text="destPathError"></p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Specify destination path within the provider</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -703,4 +912,62 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ formvalidation() {
|
||||||
|
<script>
|
||||||
|
document.addEventListener('htmx:beforeRequest', function(evt) {
|
||||||
|
if (evt.detail.elt.id === 'config-form') {
|
||||||
|
const formErrors = document.getElementById('form-errors');
|
||||||
|
const errorList = document.getElementById('error-list');
|
||||||
|
let errors = [];
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
errorList.innerHTML = '';
|
||||||
|
formErrors.classList.add('hidden');
|
||||||
|
|
||||||
|
// Validate name
|
||||||
|
const name = document.getElementById('name').value;
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
errors.push('Configuration name is required');
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get form data to check provider usage
|
||||||
|
const useSourceProvider = document.getElementById('use_source_provider')?.checked || false;
|
||||||
|
const useDestinationProvider = document.getElementById('use_destination_provider')?.checked || false;
|
||||||
|
|
||||||
|
// Validate source provider selection if using provider
|
||||||
|
if (useSourceProvider) {
|
||||||
|
const sourceProviderId = document.getElementById('source_provider_id').value;
|
||||||
|
if (!sourceProviderId || sourceProviderId === '') {
|
||||||
|
errors.push('Source provider selection is required');
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate destination provider selection if using provider
|
||||||
|
if (useDestinationProvider) {
|
||||||
|
const destProviderId = document.getElementById('destination_provider_id').value;
|
||||||
|
if (!destProviderId || destProviderId === '') {
|
||||||
|
errors.push('Destination provider selection is required');
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display errors if any
|
||||||
|
if (hasErrors) {
|
||||||
|
formErrors.classList.remove('hidden');
|
||||||
|
errors.forEach(function(error) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = error;
|
||||||
|
errorList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
evt.preventDefault(); // Prevent form submission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
@@ -394,7 +394,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Google Drive Authentication Badge -->
|
<!-- Google Drive Authentication Badge -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "drive" || config.SourceType == "drive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
||||||
<span class="ml-2 bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300">
|
<span class="ml-2 bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300">
|
||||||
<i class="fas fa-exclamation-triangle w-3 h-3 mr-1 inline"></i>
|
<i class="fas fa-exclamation-triangle w-3 h-3 mr-1 inline"></i>
|
||||||
Authentication Required
|
Authentication Required
|
||||||
@@ -402,7 +402,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Google Drive Authentication Status Indicator -->
|
<!-- Google Drive Authentication Status Indicator -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "drive" || config.SourceType == "drive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && config.GetGoogleAuthenticated() {
|
||||||
<span class="ml-2 bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
<span class="ml-2 bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
<i class="fas fa-check-circle w-3 h-3 mr-1 inline"></i>
|
<i class="fas fa-check-circle w-3 h-3 mr-1 inline"></i>
|
||||||
Authenticated
|
Authenticated
|
||||||
@@ -411,7 +411,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
<!-- Google Drive Authentication Button -->
|
<!-- Google Drive Authentication Button -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "drive" || config.SourceType == "drive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="text-yellow-700 bg-yellow-100 hover:bg-yellow-200 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-yellow-900 dark:text-yellow-300 dark:hover:bg-yellow-800 dark:focus:ring-yellow-800">
|
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="text-yellow-700 bg-yellow-100 hover:bg-yellow-200 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-yellow-900 dark:text-yellow-300 dark:hover:bg-yellow-800 dark:focus:ring-yellow-800">
|
||||||
<i class="fas fa-key w-3.5 h-3.5 mr-1.5"></i>
|
<i class="fas fa-key w-3.5 h-3.5 mr-1.5"></i>
|
||||||
Authenticate
|
Authenticate
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type DashboardData struct {
|
|||||||
FailedTransfers int
|
FailedTransfers int
|
||||||
Configs map[uint]db.TransferConfig
|
Configs map[uint]db.TransferConfig
|
||||||
RcloneVersion string
|
RcloneVersion string
|
||||||
|
LatestVersion string
|
||||||
|
CurrentVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRcloneVersion executes the rclone --version command and returns the version string
|
// GetRcloneVersion executes the rclone --version command and returns the version string
|
||||||
@@ -41,6 +43,64 @@ func GetRcloneVersion() string {
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNewerVersionAvailable checks if the latest version is newer than the current version
|
||||||
|
func isNewerVersionAvailable(current, latest string) bool {
|
||||||
|
// If either version is empty, we can't do a comparison
|
||||||
|
if current == "" || latest == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for DEV versions - always show update available
|
||||||
|
if current == "DEV" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip 'v' prefix if present for comparison
|
||||||
|
if strings.HasPrefix(current, "v") {
|
||||||
|
current = current[1:]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(latest, "v") {
|
||||||
|
latest = latest[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split versions into components
|
||||||
|
currentParts := strings.Split(current, ".")
|
||||||
|
latestParts := strings.Split(latest, ".")
|
||||||
|
|
||||||
|
// Handle non-semver format in either version
|
||||||
|
if len(currentParts) < 2 || len(latestParts) < 2 {
|
||||||
|
// If format doesn't match semver pattern, do string comparison
|
||||||
|
return current != latest && latest != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare major, minor, patch versions
|
||||||
|
for i := 0; i < len(currentParts) && i < len(latestParts); i++ {
|
||||||
|
// Parse to integers
|
||||||
|
currentNum, err1 := strconv.Atoi(currentParts[i])
|
||||||
|
latestNum, err2 := strconv.Atoi(latestParts[i])
|
||||||
|
|
||||||
|
// If either can't be parsed, do string comparison
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
if currentParts[i] < latestParts[i] {
|
||||||
|
return true
|
||||||
|
} else if currentParts[i] > latestParts[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare numbers
|
||||||
|
if latestNum > currentNum {
|
||||||
|
return true
|
||||||
|
} else if latestNum < currentNum {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all components are equal but latest has more components, it's newer
|
||||||
|
return len(latestParts) > len(currentParts)
|
||||||
|
}
|
||||||
|
|
||||||
templ Dashboard(ctx context.Context, data DashboardData) {
|
templ Dashboard(ctx context.Context, data DashboardData) {
|
||||||
@LayoutWithContext("Dashboard", ctx) {
|
@LayoutWithContext("Dashboard", ctx) {
|
||||||
<div id="dashboard-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
<div id="dashboard-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||||
@@ -253,6 +313,31 @@ templ Dashboard(ctx context.Context, data DashboardData) {
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Application Version</span>
|
||||||
|
if data.CurrentVersion != "" && data.LatestVersion != "" && isNewerVersionAvailable(data.CurrentVersion, data.LatestVersion) {
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300 mr-2">
|
||||||
|
<i class="fas fa-exclamation-triangle w-3 h-3 mr-1"></i>
|
||||||
|
{ data.CurrentVersion }
|
||||||
|
</span>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("https://github.com/starfleetcptn/gomft/releases/tag/%s", data.LatestVersion)) } target="_blank"
|
||||||
|
class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800">
|
||||||
|
<i class="fas fa-arrow-circle-up w-3 h-3 mr-1"></i>
|
||||||
|
{ data.LatestVersion } Available
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
|
<i class="fas fa-check-circle w-3 h-3 mr-1"></i>
|
||||||
|
if data.CurrentVersion != "" {
|
||||||
|
{ data.CurrentVersion }
|
||||||
|
} else {
|
||||||
|
Up to date
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -177,12 +177,15 @@ templ NotificationDropdown(data NotificationsData) {
|
|||||||
|
|
||||||
templ NotificationCount(count int64) {
|
templ NotificationCount(count int64) {
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
<div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full -top-1 -right-1" id="notification-count">
|
<div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 border border-white dark:border-gray-800 rounded-full -translate-y-1/2 translate-x-1/2" style="top: 0; right: 0;" id="notification-count">
|
||||||
if count > 99 {
|
if count > 99 {
|
||||||
99+
|
99+
|
||||||
} else {
|
} else {
|
||||||
{ fmt.Sprintf("%d", count) }
|
{ fmt.Sprintf("%d", count) }
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
} else {
|
||||||
|
<!-- Empty element to ensure proper swap when count is zero -->
|
||||||
|
<span class="hidden"></span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GDriveHeadlessAuthData contains data needed for rendering the headless auth page
|
||||||
|
type GDriveHeadlessAuthData struct {
|
||||||
|
AuthCommand string
|
||||||
|
ConfigID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GDriveHeadlessAuth renders the headless authentication page for Google Drive/Photos
|
||||||
|
templ GDriveHeadlessAuth(ctx context.Context, data GDriveHeadlessAuthData) {
|
||||||
|
// Force the layout to display as authenticated content
|
||||||
|
@LayoutWithContext("Google Authentication - Headless Mode", ctx) {
|
||||||
|
<style>
|
||||||
|
/* Ensure proper styling for the headless auth page */
|
||||||
|
body.dark .auth-page {
|
||||||
|
background-color: #111827 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="auth-container" class="auth-page w-full pb-8 bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; background-color: rgb(249, 250, 251);">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-key w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Headless Google Authentication
|
||||||
|
</h1>
|
||||||
|
<a href="/configs" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||||
|
Back to Configurations
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 mb-6">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0 mt-0.5">
|
||||||
|
<i class="fas fa-info-circle h-5 w-5 text-blue-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
You need to authenticate with Google using a web browser. Since you're running GoMFT behind a reverse proxy or in a headless environment, you'll need to complete authentication on a machine with a web browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 1: Run the following command on a machine with a web browser</h2>
|
||||||
|
<div class="relative mb-4">
|
||||||
|
<pre id="auth-command-text" class="bg-gray-50 dark:bg-gray-900 rounded-md p-4 overflow-x-auto text-sm font-mono">{ data.AuthCommand }</pre>
|
||||||
|
<button id="copy-command" class="absolute top-2 right-2 bg-gray-200 dark:bg-gray-700 p-1.5 rounded hover:bg-gray-300 dark:hover:bg-gray-600" title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy h-5 w-5 text-gray-700 dark:text-gray-300"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-md font-medium mb-2 text-gray-900 dark:text-white">What this command does:</h3>
|
||||||
|
<ul class="list-disc ml-6 text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||||
|
<li>Opens a browser window on the machine where you run it</li>
|
||||||
|
<li>Allows you to authenticate with Google</li>
|
||||||
|
<li>Generates an authentication token</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 2: Paste the authentication token below</h2>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
After completing authentication in the browser, you'll receive a token. Copy and paste that token here:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="/configs/gdrive-headless-token" method="POST" class="space-y-4">
|
||||||
|
<input type="hidden" name="config_id" value={ data.ConfigID } />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="auth_token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Authentication Token</label>
|
||||||
|
<textarea
|
||||||
|
id="auth_token"
|
||||||
|
name="auth_token"
|
||||||
|
rows="5"
|
||||||
|
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
|
||||||
|
placeholder="Paste your authentication token here..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">The token will look like a long JSON string containing access credentials.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-6">
|
||||||
|
<a href="/configs" class="mr-4 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-400">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Submit Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">This authentication process is necessary for GoMFT to access your Google Drive or Google Photos account.</p>
|
||||||
|
<p class="mt-1 text-gray-600 dark:text-gray-400">The token is only used for authentication and is stored securely. You'll only need to complete this process once for each configuration.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('auth-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('auth-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the actual command text
|
||||||
|
const actualCommand = document.getElementById('auth-command-text').textContent.trim();
|
||||||
|
|
||||||
|
// Add click handler to copy button
|
||||||
|
document.getElementById('copy-command').addEventListener('click', function() {
|
||||||
|
// Copy the actual command text, not the template variable
|
||||||
|
navigator.clipboard.writeText(actualCommand).then(function() {
|
||||||
|
// Show a success message
|
||||||
|
const button = document.getElementById('copy-command');
|
||||||
|
const originalTitle = button.getAttribute('title');
|
||||||
|
button.setAttribute('title', 'Copied!');
|
||||||
|
|
||||||
|
// Also show visual feedback
|
||||||
|
button.classList.add('bg-green-200', 'dark:bg-green-700');
|
||||||
|
button.classList.remove('bg-gray-200', 'dark:bg-gray-700');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
button.setAttribute('title', originalTitle);
|
||||||
|
button.classList.remove('bg-green-200', 'dark:bg-green-700');
|
||||||
|
button.classList.add('bg-gray-200', 'dark:bg-gray-700');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to copy text: ', err);
|
||||||
|
alert('Failed to copy command. Please select and copy it manually.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,27 +151,35 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<div class="flex flex-col flex-grow pt-5 bg-white dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
|
<div class="flex flex-col flex-grow pt-5 bg-white dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
|
||||||
<div class="flex items-center flex-shrink-0 px-4">
|
<div class="flex items-center flex-shrink-0 px-4">
|
||||||
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||||
<i class="fas fa-exchange-alt mr-2"></i>
|
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
|
||||||
GoMFT
|
GoMFT
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 px-2 py-4 bg-white dark:bg-gray-800">
|
<nav class="flex-1 px-2 py-4 bg-white dark:bg-gray-800">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-chart-pie mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/storage-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-server mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Storage Providers
|
||||||
|
</a>
|
||||||
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Configs
|
Transfer Configurations
|
||||||
</a>
|
</a>
|
||||||
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Jobs
|
Scheduled Jobs
|
||||||
|
</a>
|
||||||
|
<a href="/calendar" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-calendar-week mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Transfer Calendar
|
||||||
</a>
|
</a>
|
||||||
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
History
|
Transfer History
|
||||||
</a>
|
</a>
|
||||||
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
@@ -183,16 +191,20 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
</p>
|
</p>
|
||||||
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Users
|
User Management
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Roles
|
Role Management
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Audit Logs
|
Audit Logs
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/logs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Log Viewer
|
||||||
|
</a>
|
||||||
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Database Tools
|
Database Tools
|
||||||
@@ -246,8 +258,8 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
|
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||||
<i class="fas fa-exchange-alt mr-2"></i>
|
<!-- GoMFT Logo -->
|
||||||
GoMFT
|
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
|
||||||
</a>
|
</a>
|
||||||
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -258,20 +270,28 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<!-- Copy the same links from the desktop sidebar -->
|
<!-- Copy the same links from the desktop sidebar -->
|
||||||
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-chart-pie mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/storage-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-server mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Storage Providers
|
||||||
|
</a>
|
||||||
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Configs
|
Transfer Configurations
|
||||||
</a>
|
</a>
|
||||||
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
Jobs
|
Scheduled Jobs
|
||||||
|
</a>
|
||||||
|
<a href="/calendar" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-calendar-week mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Transfer Calendar
|
||||||
</a>
|
</a>
|
||||||
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
History
|
Transfer History
|
||||||
</a>
|
</a>
|
||||||
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
@@ -283,29 +303,33 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
</p>
|
</p>
|
||||||
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Users
|
User Management
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Roles
|
Role Management
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Audit Logs
|
Audit Logs
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/logs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<i class="fas fa-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Log Viewer
|
||||||
|
</a>
|
||||||
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
Database Tools
|
Database Tools
|
||||||
</a>
|
</a>
|
||||||
// Settings Dropdown
|
// Settings Dropdown
|
||||||
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings-mobile" data-collapse-toggle="dropdown-settings-mobile">
|
||||||
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||||
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
||||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
<ul id="dropdown-settings-mobile" class="hidden py-2 space-y-2">
|
||||||
// <li>
|
// <li>
|
||||||
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
||||||
// </li>
|
// </li>
|
||||||
@@ -332,7 +356,7 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<header class="bg-white dark:bg-gray-800 shadow-sm">
|
<header class="bg-white dark:bg-gray-800 shadow-sm">
|
||||||
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
// <h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
||||||
</div>
|
</div>
|
||||||
<!-- Theme toggle and user menu -->
|
<!-- Theme toggle and user menu -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
@@ -356,44 +380,45 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
hx-get="/notifications/dropdown"
|
hx-get="/notifications/dropdown"
|
||||||
hx-trigger="click once"
|
hx-trigger="click once"
|
||||||
hx-target="#notification-dropdown-content"
|
hx-target="#notification-dropdown-content"
|
||||||
data-dropdown-placement="bottom-end"
|
data-dropdown-placement="bottom-start"
|
||||||
>
|
>
|
||||||
<span class="sr-only">View notifications</span>
|
<span class="sr-only">View notifications</span>
|
||||||
<i class="fas fa-bell"></i>
|
<i class="fas fa-bell"></i>
|
||||||
<!-- Notification badge will be loaded dynamically -->
|
<!-- Notification count will be placed here via HTMX -->
|
||||||
<div hx-get="/notifications/count" hx-trigger="load, notification-updated from:body" id="notification-count-container"></div>
|
|
||||||
</button>
|
</button>
|
||||||
<!-- Notification dropdown -->
|
<!-- Place the notification count container outside but still in the relative container -->
|
||||||
<div
|
<div id="notification-count-container" class="absolute top-0 right-0" hx-get="/notifications/count" hx-trigger="load, notification-updated from:body"></div>
|
||||||
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl"
|
</div>
|
||||||
id="notification-dropdown"
|
<!-- Notification dropdown -->
|
||||||
style="min-width: 320px; width: 100%;"
|
<div
|
||||||
data-dropdown-placement="bottom-end"
|
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl fixed"
|
||||||
>
|
id="notification-dropdown"
|
||||||
<div id="notification-dropdown-content">
|
style="min-width: 320px; width: 100%;"
|
||||||
<!-- Content will be loaded dynamically via HTMX -->
|
data-dropdown-placement="bottom-start"
|
||||||
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
>
|
||||||
Notifications
|
<div id="notification-dropdown-content">
|
||||||
</div>
|
<!-- Content will be loaded dynamically via HTMX -->
|
||||||
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
||||||
<div class="animate-pulse flex flex-col items-center">
|
Notifications
|
||||||
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
|
||||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
View all
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="animate-pulse flex flex-col items-center">
|
||||||
|
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||||
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||||
|
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
View all
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- User menu dropdown -->
|
<!-- User menu dropdown -->
|
||||||
@@ -466,6 +491,9 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
<i class="fab fa-github"></i>
|
<i class="fab fa-github"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
</a>
|
||||||
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
<i class="fas fa-tag"></i> { AppVersion }
|
<i class="fas fa-tag"></i> { AppVersion }
|
||||||
</a>
|
</a>
|
||||||
@@ -489,6 +517,9 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
<i class="fab fa-github"></i>
|
<i class="fab fa-github"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
</a>
|
||||||
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
<i class="fas fa-tag"></i> { AppVersion }
|
<i class="fas fa-tag"></i> { AppVersion }
|
||||||
</a>
|
</a>
|
||||||
@@ -506,6 +537,26 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
|||||||
<!-- Application scripts -->
|
<!-- Application scripts -->
|
||||||
<script defer src="/static/dist/app.js"></script>
|
<script defer src="/static/dist/app.js"></script>
|
||||||
<script defer src="/static/dist/init.js"></script>
|
<script defer src="/static/dist/init.js"></script>
|
||||||
|
<!-- Dropdown fix for mobile -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Initialize the mobile dropdown separately
|
||||||
|
const mobileDropdownButton = document.querySelector('[data-collapse-toggle="dropdown-settings-mobile"]');
|
||||||
|
const mobileDropdown = document.getElementById("dropdown-settings-mobile");
|
||||||
|
|
||||||
|
if (mobileDropdownButton && mobileDropdown) {
|
||||||
|
// Show dropdown if on admin pages
|
||||||
|
if (window.location.pathname.startsWith("/admin")) {
|
||||||
|
// Keep it hidden by default, will be toggled by button
|
||||||
|
mobileDropdown.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileDropdownButton.addEventListener("click", function() {
|
||||||
|
mobileDropdown.classList.toggle("hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,985 +0,0 @@
|
|||||||
package components
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationFormData struct {
|
|
||||||
NotificationService *struct {
|
|
||||||
ID uint
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Type string
|
|
||||||
IsEnabled bool
|
|
||||||
EventTriggers []string
|
|
||||||
RetryPolicy string
|
|
||||||
WebhookURL string
|
|
||||||
Method string
|
|
||||||
Headers string
|
|
||||||
PayloadTemplate string
|
|
||||||
SecretKey string
|
|
||||||
PushbulletAPIKey string
|
|
||||||
PushbulletDeviceID string
|
|
||||||
PushbulletTitleTemplate string
|
|
||||||
PushbulletBodyTemplate string
|
|
||||||
NtfyServer string
|
|
||||||
NtfyTopic string
|
|
||||||
NtfyPriority string
|
|
||||||
NtfyUsername string
|
|
||||||
NtfyPassword string
|
|
||||||
NtfyTitleTemplate string
|
|
||||||
NtfyMessageTemplate string
|
|
||||||
GotifyURL string
|
|
||||||
GotifyToken string
|
|
||||||
GotifyPriority string
|
|
||||||
GotifyTitleTemplate string
|
|
||||||
GotifyMessageTemplate string
|
|
||||||
PushoverAPIToken string
|
|
||||||
PushoverUserKey string
|
|
||||||
PushoverDevice string
|
|
||||||
PushoverPriority string
|
|
||||||
PushoverSound string
|
|
||||||
PushoverTitleTemplate string
|
|
||||||
PushoverMessageTemplate string
|
|
||||||
}
|
|
||||||
IsNew bool
|
|
||||||
SuccessMessage string
|
|
||||||
ErrorMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if a string is in a slice
|
|
||||||
func contains(slice []string, str string) bool {
|
|
||||||
for _, s := range slice {
|
|
||||||
if s == str {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert bool to string for HTML attributes
|
|
||||||
func boolToString(b bool) string {
|
|
||||||
if b {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNotificationFormTitle(isNew bool) string {
|
|
||||||
if isNew {
|
|
||||||
return "Add Notification Service"
|
|
||||||
}
|
|
||||||
return "Edit Notification Service"
|
|
||||||
}
|
|
||||||
|
|
||||||
templ NotificationForm(ctx context.Context, data NotificationFormData) {
|
|
||||||
@LayoutWithContext(getNotificationFormTitle(data.IsNew), ctx) {
|
|
||||||
<!-- Status and Error Messages -->
|
|
||||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Notification system
|
|
||||||
function showToast(message, type) {
|
|
||||||
const toastContainer = document.getElementById('toast-container');
|
|
||||||
|
|
||||||
// Create toast element
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.id = 'toast-' + type + '-' + Date.now();
|
|
||||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
|
||||||
toast.role = 'alert';
|
|
||||||
|
|
||||||
// Set toast content based on type
|
|
||||||
let iconClass, bgColorClass, textColorClass;
|
|
||||||
|
|
||||||
if (type === 'success') {
|
|
||||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
|
||||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
|
||||||
textColorClass = 'text-green-500 dark:text-green-200';
|
|
||||||
} else if (type === 'error') {
|
|
||||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
|
||||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
|
||||||
textColorClass = 'text-red-500 dark:text-red-200';
|
|
||||||
} else {
|
|
||||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
|
||||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
|
||||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set inner HTML with appropriate icon and message
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
|
||||||
${type === 'success'
|
|
||||||
? '<i class="fas fa-check"></i>'
|
|
||||||
: type === 'error'
|
|
||||||
? '<i class="fas fa-exclamation-circle"></i>'
|
|
||||||
: '<i class="fas fa-info-circle"></i>'}
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
|
||||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
|
||||||
<span class="sr-only">Close</span>
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add toast to container
|
|
||||||
toastContainer.appendChild(toast);
|
|
||||||
|
|
||||||
// Trigger animation after a small delay to ensure the DOM has updated
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
|
||||||
toast.classList.add('translate-y-0', 'opacity-100');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// Add event listener to close button
|
|
||||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
|
||||||
closeButton.addEventListener('click', function() {
|
|
||||||
// Animate out before removing
|
|
||||||
toast.classList.add('opacity-0', 'translate-y-4');
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.remove();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-remove toast after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add('opacity-0', 'translate-y-4');
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.remove();
|
|
||||||
}, 300);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle notification fields based on selection
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const typeSelector = document.getElementById('notification_type');
|
|
||||||
const allFields = document.querySelectorAll('.notification-fields');
|
|
||||||
const commonFields = document.querySelectorAll('.common-fields');
|
|
||||||
|
|
||||||
typeSelector.addEventListener('change', function() {
|
|
||||||
// Hide all fields first
|
|
||||||
allFields.forEach(field => field.classList.add('hidden'));
|
|
||||||
|
|
||||||
// Show/hide common fields based on selection
|
|
||||||
const selectedType = this.value;
|
|
||||||
if (selectedType) {
|
|
||||||
// Show common fields (name, description)
|
|
||||||
commonFields.forEach(field => field.classList.remove('hidden'));
|
|
||||||
|
|
||||||
// Show the selected type's specific fields
|
|
||||||
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
|
||||||
if (fieldsToShow) {
|
|
||||||
fieldsToShow.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Hide common fields if no type selected
|
|
||||||
commonFields.forEach(field => field.classList.add('hidden'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize form if editing
|
|
||||||
if (document.getElementById('notification_type').value) {
|
|
||||||
// Trigger the change event to show the appropriate fields
|
|
||||||
document.getElementById('notification_type').dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="notification-form-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div class="pb-8 w-full max-w-4xl mx-auto">
|
|
||||||
<!-- Success Message (hidden, used for HTMX responses) -->
|
|
||||||
if data.SuccessMessage != "" {
|
|
||||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
|
||||||
}
|
|
||||||
<!-- Error Message (hidden, used for HTMX responses) -->
|
|
||||||
if data.ErrorMessage != "" {
|
|
||||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
||||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
|
||||||
{ getNotificationFormTitle(data.IsNew) }
|
|
||||||
</h1>
|
|
||||||
<a href="/admin/settings/notifications" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
|
||||||
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
|
||||||
Back to Notification Services
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Notification Service Form -->
|
|
||||||
<div class="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
||||||
<form id="notification-form"
|
|
||||||
if data.IsNew {
|
|
||||||
hx-post="/admin/settings/notifications"
|
|
||||||
} else {
|
|
||||||
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
|
||||||
}
|
|
||||||
hx-target="body">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
|
||||||
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="">Select a type</option>
|
|
||||||
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
|
||||||
<option value="webhook" selected="selected">Webhook</option>
|
|
||||||
} else {
|
|
||||||
<option value="webhook">Webhook</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
|
||||||
<option value="pushbullet" selected="selected">Pushbullet</option>
|
|
||||||
} else {
|
|
||||||
<option value="pushbullet">Pushbullet</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
|
||||||
<option value="ntfy" selected="selected">Ntfy</option>
|
|
||||||
} else {
|
|
||||||
<option value="ntfy">Ntfy</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
|
||||||
<option value="gotify" selected="selected">Gotify</option>
|
|
||||||
} else {
|
|
||||||
<option value="gotify">Gotify</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
|
||||||
<option value="pushover" selected="selected">Pushover</option>
|
|
||||||
} else {
|
|
||||||
<option value="pushover">Pushover</option>
|
|
||||||
}
|
|
||||||
<option value="email" disabled>Email (Coming Soon)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6 hidden common-fields">
|
|
||||||
<label for="notification_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
|
||||||
if data.NotificationService.Name != "" {
|
|
||||||
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value={ data.NotificationService.Name }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="notification_name" name="name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Notification Service" required value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6 hidden common-fields">
|
|
||||||
<label for="notification_description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
|
||||||
if data.NotificationService.Description != "" {
|
|
||||||
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service">{ data.NotificationService.Description }</textarea>
|
|
||||||
} else {
|
|
||||||
<textarea id="notification_description" name="description" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Description for this notification service"></textarea>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dynamic fields based on notification type -->
|
|
||||||
<div id="email_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="smtp_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Host</label>
|
|
||||||
<input type="text" id="smtp_host" name="smtp_host" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="smtp.example.com"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="smtp_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Port</label>
|
|
||||||
<input type="number" id="smtp_port" name="smtp_port" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="587"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="smtp_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Username</label>
|
|
||||||
<input type="text" id="smtp_username" name="smtp_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="user@example.com"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="smtp_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Password</label>
|
|
||||||
<input type="password" id="smtp_password" name="smtp_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="from_email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Email</label>
|
|
||||||
<input type="email" id="from_email" name="from_email" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="notifications@example.com"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="webhook_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="webhook_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Webhook URL</label>
|
|
||||||
if data.NotificationService.WebhookURL != "" {
|
|
||||||
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value={ data.NotificationService.WebhookURL }/>
|
|
||||||
} else {
|
|
||||||
<input type="url" id="webhook_url" name="webhook_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://api.example.com/webhook" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="method" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">HTTP Method</label>
|
|
||||||
<select id="method" name="method" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
if data.NotificationService.Method != "" {
|
|
||||||
if data.NotificationService.Method == "POST" {
|
|
||||||
<option value="POST" selected="selected">POST</option>
|
|
||||||
} else {
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService.Method == "PUT" {
|
|
||||||
<option value="PUT" selected="selected">PUT</option>
|
|
||||||
} else {
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="headers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Headers (JSON)</label>
|
|
||||||
if data.NotificationService.Headers != "" {
|
|
||||||
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'>{ data.NotificationService.Headers }</textarea>
|
|
||||||
} else {
|
|
||||||
<textarea id="headers" name="headers" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="payload_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Payload Template (JSON)</label>
|
|
||||||
<textarea
|
|
||||||
id="payload_template"
|
|
||||||
name="payload_template"
|
|
||||||
rows="5"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder='{
|
|
||||||
"event": "{{job.event}}",
|
|
||||||
"job": {
|
|
||||||
"id": "{{job.id}}",
|
|
||||||
"name": "{{job.name}}",
|
|
||||||
"status": "{{job.status}}",
|
|
||||||
"message": "{{job.message}}",
|
|
||||||
"started_at": "{{job.started_at}}",
|
|
||||||
"completed_at": "{{job.completed_at}}",
|
|
||||||
"duration_seconds": {{job.duration_seconds}},
|
|
||||||
"config_id": "{{job.config_id}}",
|
|
||||||
"config_name": "{{job.config_name}}",
|
|
||||||
"transfer_bytes": {{job.transfer_bytes}},
|
|
||||||
"file_count": {{job.file_count}}
|
|
||||||
},
|
|
||||||
"instance": {
|
|
||||||
"id": "{{instance.id}}",
|
|
||||||
"name": "{{instance.name}}",
|
|
||||||
"version": "{{instance.version}}",
|
|
||||||
"environment": "{{instance.environment}}"
|
|
||||||
},
|
|
||||||
"timestamp": "{{timestamp}}",
|
|
||||||
"notification_id": "{{notification.id}}"
|
|
||||||
}'
|
|
||||||
>
|
|
||||||
if data.NotificationService.PayloadTemplate != "" {
|
|
||||||
data.NotificationService.PayloadTemplate
|
|
||||||
} </textarea>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
|
||||||
}
|
|
||||||
<label for="trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
|
||||||
}
|
|
||||||
<label for="trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
|
||||||
}
|
|
||||||
<label for="trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (for signature verification)</label>
|
|
||||||
if data.NotificationService.SecretKey != "" {
|
|
||||||
<input type="text" id="secret_key" name="secret_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Optional signature verification key" value={ data.NotificationService.SecretKey }/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">If provided, all webhooks will include an X-GoMFT-Signature header</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="retry_policy" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Retry Policy</label>
|
|
||||||
<select id="retry_policy" name="retry_policy" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
if data.NotificationService.RetryPolicy != "" {
|
|
||||||
if data.NotificationService.RetryPolicy == "none" {
|
|
||||||
<option value="none" selected="selected">No retries</option>
|
|
||||||
} else {
|
|
||||||
<option value="none">No retries</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService.RetryPolicy == "simple" {
|
|
||||||
<option value="simple" selected="selected">Simple (3 retries)</option>
|
|
||||||
} else {
|
|
||||||
<option value="simple">Simple (3 retries)</option>
|
|
||||||
}
|
|
||||||
if data.NotificationService.RetryPolicy == "exponential" {
|
|
||||||
<option value="exponential" selected="selected">Exponential backoff</option>
|
|
||||||
} else {
|
|
||||||
<option value="exponential">Exponential backoff</option>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<option value="none">No retries</option>
|
|
||||||
<option value="simple">Simple (3 retries)</option>
|
|
||||||
<option value="exponential">Exponential backoff</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<!-- Test notification button -->
|
|
||||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="test-webhook-btn"
|
|
||||||
hx-post="/admin/settings/notifications/test"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#test-notification-result"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane mr-1"></i>
|
|
||||||
Send Test Notification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Send a test notification to verify your configuration works correctly before saving.
|
|
||||||
</p>
|
|
||||||
<div id="test-notification-result" class="mt-3 hidden">
|
|
||||||
<!-- Result will be shown here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pushbullet Fields -->
|
|
||||||
<div id="pushbullet_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushbullet_api_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Key</label>
|
|
||||||
if data.NotificationService.PushbulletAPIKey != "" {
|
|
||||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value={ data.NotificationService.PushbulletAPIKey }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="o.XyzAbCdEfGhIjKlMnOpQrSt" value=""/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get your API key from <a href="https://www.pushbullet.com/#settings/account" target="_blank" class="text-blue-500 hover:underline">Pushbullet Account Settings</a></p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushbullet_device_iden" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Identifier (Optional)</label>
|
|
||||||
if data.NotificationService.PushbulletDeviceID != "" {
|
|
||||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushbulletDeviceID }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushbullet_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
|
||||||
if data.NotificationService.PushbulletTitleTemplate != "" {
|
|
||||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushbulletTitleTemplate }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushbullet_body_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
|
||||||
<textarea
|
|
||||||
id="pushbullet_body_template"
|
|
||||||
name="pushbullet_body_template"
|
|
||||||
rows="4"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
|
||||||
>
|
|
||||||
if data.NotificationService.PushbulletBodyTemplate != "" {
|
|
||||||
data.NotificationService.PushbulletBodyTemplate
|
|
||||||
} </textarea>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pb_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pb_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pb_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Test notification button for Pushbullet -->
|
|
||||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="test-pushbullet-btn"
|
|
||||||
hx-post="/admin/settings/notifications/test"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#test-notification-result"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane mr-1"></i>
|
|
||||||
Send Test Notification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Send a test notification to verify your Pushbullet configuration works correctly before saving.
|
|
||||||
</p>
|
|
||||||
<div id="test-notification-result" class="mt-3 hidden">
|
|
||||||
<!-- Result will be shown here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ntfy Fields -->
|
|
||||||
<div id="ntfy_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_server" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Ntfy Server</label>
|
|
||||||
if data.NotificationService.NtfyServer != "" {
|
|
||||||
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value={ data.NotificationService.NtfyServer }/>
|
|
||||||
} else {
|
|
||||||
<input type="url" id="ntfy_server" name="ntfy_server" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://ntfy.sh" value="https://ntfy.sh"/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The Ntfy server URL (default: ntfy.sh)</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_topic" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Topic</label>
|
|
||||||
if data.NotificationService.NtfyTopic != "" {
|
|
||||||
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value={ data.NotificationService.NtfyTopic }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="ntfy_topic" name="ntfy_topic" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="your-unique-topic" value="gomft"/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a unique, unguessable topic name</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
|
||||||
<select id="ntfy_priority" name="ntfy_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="1">Low (1)</option>
|
|
||||||
if data.NotificationService.NtfyPriority == "3" {
|
|
||||||
<option value="3" selected="selected">Default (3)</option>
|
|
||||||
} else {
|
|
||||||
<option value="3">Default (3)</option>
|
|
||||||
}
|
|
||||||
<option value="4">High (4)</option>
|
|
||||||
<option value="5">Urgent (5)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username (Optional)</label>
|
|
||||||
if data.NotificationService.NtfyUsername != "" {
|
|
||||||
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value={ data.NotificationService.NtfyUsername }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="ntfy_username" name="ntfy_username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Username for protected topics" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password (Optional)</label>
|
|
||||||
if data.NotificationService.NtfyPassword != "" {
|
|
||||||
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value={ data.NotificationService.NtfyPassword }/>
|
|
||||||
} else {
|
|
||||||
<input type="password" id="ntfy_password" name="ntfy_password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Password for protected topics" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
|
||||||
if data.NotificationService.NtfyTitleTemplate != "" {
|
|
||||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.NtfyTitleTemplate }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="ntfy_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
|
||||||
<textarea
|
|
||||||
id="ntfy_message_template"
|
|
||||||
name="ntfy_message_template"
|
|
||||||
rows="4"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
|
||||||
>
|
|
||||||
if data.NotificationService.NtfyMessageTemplate != "" {
|
|
||||||
data.NotificationService.NtfyMessageTemplate
|
|
||||||
} </textarea>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="ntfy_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="ntfy_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="ntfy_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Test notification button for Ntfy -->
|
|
||||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="test-ntfy-btn"
|
|
||||||
hx-post="/admin/settings/notifications/test"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#test-notification-result"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane mr-1"></i>
|
|
||||||
Send Test Notification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Send a test notification to verify your Ntfy configuration works correctly before saving.
|
|
||||||
</p>
|
|
||||||
<div id="test-notification-result" class="mt-3 hidden">
|
|
||||||
<!-- Result will be shown here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gotify Fields -->
|
|
||||||
<div id="gotify_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="gotify_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Gotify Server URL</label>
|
|
||||||
if data.NotificationService.GotifyURL != "" {
|
|
||||||
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value={ data.NotificationService.GotifyURL }/>
|
|
||||||
} else {
|
|
||||||
<input type="url" id="gotify_url" name="gotify_url" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="https://gotify.example.com" value=""/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">URL of your Gotify server</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="gotify_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Token</label>
|
|
||||||
if data.NotificationService.GotifyToken != "" {
|
|
||||||
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value={ data.NotificationService.GotifyToken }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="gotify_token" name="gotify_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="A-M-XiEQj.zX5d" value=""/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find this in your Gotify application settings</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="gotify_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
|
||||||
<select id="gotify_priority" name="gotify_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="0">Low (0)</option>
|
|
||||||
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
|
||||||
<option value="5" selected="selected">Normal (5)</option>
|
|
||||||
} else {
|
|
||||||
<option value="5">Normal (5)</option>
|
|
||||||
}
|
|
||||||
<option value="8">High (8)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="gotify_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
|
||||||
if data.NotificationService.GotifyTitleTemplate != "" {
|
|
||||||
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.GotifyTitleTemplate }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="gotify_title_template" name="gotify_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{{job.name}} {{job.status}}" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="gotify_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
|
||||||
<textarea
|
|
||||||
id="gotify_message_template"
|
|
||||||
name="gotify_message_template"
|
|
||||||
rows="4"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
|
||||||
>
|
|
||||||
if data.NotificationService.GotifyMessageTemplate != "" {
|
|
||||||
data.NotificationService.GotifyMessageTemplate
|
|
||||||
}</textarea>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="gotify_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="gotify_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="gotify_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Test notification button for Gotify -->
|
|
||||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="test-gotify-btn"
|
|
||||||
hx-post="/admin/settings/notifications/test"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#test-notification-result"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane mr-1"></i>
|
|
||||||
Send Test Notification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Send a test notification to verify your Gotify configuration works correctly before saving.
|
|
||||||
</p>
|
|
||||||
<div id="test-notification-result" class="mt-3 hidden">
|
|
||||||
<!-- Result will be shown here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pushover Fields -->
|
|
||||||
<div id="pushover_fields" class="hidden notification-fields">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_app_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Token/Key</label>
|
|
||||||
if data.NotificationService.PushoverAPIToken != "" {
|
|
||||||
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value={ data.NotificationService.PushoverAPIToken }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushover_app_token" name="pushover_app_token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value=""/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your application's API token/key from <a href="https://pushover.net/apps" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_user_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">User Key</label>
|
|
||||||
if data.NotificationService.PushoverUserKey != "" {
|
|
||||||
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value={ data.NotificationService.PushoverUserKey }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushover_user_key" name="pushover_user_key" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value=""/>
|
|
||||||
}
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your user key from <a href="https://pushover.net/" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_device" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Name (Optional)</label>
|
|
||||||
if data.NotificationService.PushoverDevice != "" {
|
|
||||||
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value={ data.NotificationService.PushoverDevice }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushover_device" name="pushover_device" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Leave empty to send to all devices" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
|
||||||
<select id="pushover_priority" name="pushover_priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="-2">Lowest (-2)</option>
|
|
||||||
<option value="-1">Low (-1)</option>
|
|
||||||
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
|
||||||
<option value="0" selected="selected">Normal (0)</option>
|
|
||||||
} else {
|
|
||||||
<option value="0">Normal (0)</option>
|
|
||||||
}
|
|
||||||
<option value="1">High (1)</option>
|
|
||||||
<option value="2">Emergency (2)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_sound" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sound</label>
|
|
||||||
<select id="pushover_sound" name="pushover_sound" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="pushover">Pushover (default)</option>
|
|
||||||
<option value="bike">Bike</option>
|
|
||||||
<option value="bugle">Bugle</option>
|
|
||||||
<option value="cashregister">Cash Register</option>
|
|
||||||
<option value="classical">Classical</option>
|
|
||||||
<option value="cosmic">Cosmic</option>
|
|
||||||
<option value="falling">Falling</option>
|
|
||||||
<option value="gamelan">Gamelan</option>
|
|
||||||
<option value="incoming">Incoming</option>
|
|
||||||
<option value="intermission">Intermission</option>
|
|
||||||
<option value="magic">Magic</option>
|
|
||||||
<option value="mechanical">Mechanical</option>
|
|
||||||
<option value="pianobar">Piano Bar</option>
|
|
||||||
<option value="siren">Siren</option>
|
|
||||||
<option value="spacealarm">Space Alarm</option>
|
|
||||||
<option value="tugboat">Tug Boat</option>
|
|
||||||
<option value="alien">Alien Alarm (long)</option>
|
|
||||||
<option value="climb">Climb (long)</option>
|
|
||||||
<option value="persistent">Persistent (long)</option>
|
|
||||||
<option value="echo">Echo (long)</option>
|
|
||||||
<option value="updown">Up Down (long)</option>
|
|
||||||
<option value="vibrate">Vibrate Only</option>
|
|
||||||
<option value="none">None (silent)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
|
||||||
if data.NotificationService.PushoverTitleTemplate != "" {
|
|
||||||
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={ data.NotificationService.PushoverTitleTemplate }/>
|
|
||||||
} else {
|
|
||||||
<input type="text" id="pushover_title_template" name="pushover_title_template" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="'{{job.name}}' {{job.status}}" value=""/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="pushover_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
|
||||||
<textarea
|
|
||||||
id="pushover_message_template"
|
|
||||||
name="pushover_message_template"
|
|
||||||
rows="4"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
|
||||||
>
|
|
||||||
if data.NotificationService.PushoverMessageTemplate != "" {
|
|
||||||
data.NotificationService.PushoverMessageTemplate
|
|
||||||
} </textarea>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pushover_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pushover_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
if data.NotificationService.EventTriggers != nil {
|
|
||||||
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
|
||||||
} else {
|
|
||||||
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
|
||||||
}
|
|
||||||
<label for="pushover_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Test notification button for Pushover -->
|
|
||||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="test-pushover-btn"
|
|
||||||
hx-post="/admin/settings/notifications/test"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#test-notification-result"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane mr-1"></i>
|
|
||||||
Send Test Notification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Send a test notification to verify your Pushover configuration works correctly before saving.
|
|
||||||
</p>
|
|
||||||
<div id="test-notification-result" class="mt-3 hidden">
|
|
||||||
<!-- Result will be shown here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-start mb-6 hidden common-fields">
|
|
||||||
<div class="flex items-center h-5">
|
|
||||||
if data.NotificationService.IsEnabled != false {
|
|
||||||
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(data.NotificationService.IsEnabled) }/>
|
|
||||||
} else {
|
|
||||||
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked="true"/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-sm">
|
|
||||||
<label for="is_enabled" class="font-medium text-gray-900 dark:text-white">Enable this notification service</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hidden common-fields">
|
|
||||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
|
||||||
if data.IsNew {
|
|
||||||
Add Service
|
|
||||||
} else {
|
|
||||||
Save Changes
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Help Notice -->
|
|
||||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
|
||||||
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Set dark background color if in dark mode
|
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
|
||||||
document.getElementById('notification-form-container').style.backgroundColor = '#111827';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listener for theme changes
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
document.getElementById('notification-form-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
package components
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dialog component for confirmation dialogs using Flowbite modal
|
|
||||||
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
|
|
||||||
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
|
||||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
|
||||||
<div class="p-6 text-center">
|
|
||||||
if action == "delete" {
|
|
||||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
|
||||||
} else {
|
|
||||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
|
||||||
}
|
|
||||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={ confirmClass }
|
|
||||||
hx-delete={ fmt.Sprintf("/admin/settings/notifications/%d", serviceID) }
|
|
||||||
hx-target="body"
|
|
||||||
data-service-name={ serviceName }
|
|
||||||
data-service-id={ fmt.Sprint(serviceID) }
|
|
||||||
id={ fmt.Sprintf("delete-btn-%d", serviceID) }
|
|
||||||
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
|
|
||||||
{ confirmText }
|
|
||||||
</button>
|
|
||||||
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
|
|
||||||
// Hide the dialog
|
|
||||||
document.getElementById(dialogId).classList.add("hidden");
|
|
||||||
document.getElementById(dialogId).classList.remove("flex");
|
|
||||||
|
|
||||||
// Add debugging info
|
|
||||||
console.log(`Notification service deletion triggered for: ${serviceName} (ID: ${serviceID})`);
|
|
||||||
|
|
||||||
// Store data in a way that's accessible to event handlers
|
|
||||||
window.lastDeletedService = {
|
|
||||||
id: serviceID,
|
|
||||||
name: serviceName
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add custom marker to track this deletion
|
|
||||||
window.currentlyDeletingService = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
|
||||||
@LayoutWithContext("Notification Services", ctx) {
|
|
||||||
<!-- Status and Error Messages -->
|
|
||||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Notification system
|
|
||||||
function showToast(message, type) {
|
|
||||||
const toastContainer = document.getElementById('toast-container');
|
|
||||||
|
|
||||||
// Create toast element
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.id = 'toast-' + type + '-' + Date.now();
|
|
||||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
|
||||||
toast.role = 'alert';
|
|
||||||
|
|
||||||
// Set toast content based on type
|
|
||||||
let iconClass, bgColorClass, textColorClass;
|
|
||||||
|
|
||||||
if (type === 'success') {
|
|
||||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
|
||||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
|
||||||
textColorClass = 'text-green-500 dark:text-green-200';
|
|
||||||
} else if (type === 'error') {
|
|
||||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
|
||||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
|
||||||
textColorClass = 'text-red-500 dark:text-red-200';
|
|
||||||
} else {
|
|
||||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
|
||||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
|
||||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set inner HTML with appropriate icon and message
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
|
||||||
${type === 'success'
|
|
||||||
? '<i class="fas fa-check"></i>'
|
|
||||||
: type === 'error'
|
|
||||||
? '<i class="fas fa-exclamation-circle"></i>'
|
|
||||||
: '<i class="fas fa-info-circle"></i>'}
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
|
||||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
|
||||||
<span class="sr-only">Close</span>
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add toast to container
|
|
||||||
toastContainer.appendChild(toast);
|
|
||||||
|
|
||||||
// Trigger animation after a small delay to ensure the DOM has updated
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
|
||||||
toast.classList.add('translate-y-0', 'opacity-100');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// Add event listener to close button
|
|
||||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
|
||||||
closeButton.addEventListener('click', function() {
|
|
||||||
// Animate out before removing
|
|
||||||
toast.classList.add('opacity-0', 'translate-y-4');
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.remove();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-remove toast after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add('opacity-0', 'translate-y-4');
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.remove();
|
|
||||||
}, 300);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track all HTMX events for debugging
|
|
||||||
document.addEventListener('htmx:beforeRequest', function(event) {
|
|
||||||
// Check if this is a DELETE request for a notification service
|
|
||||||
const path = event.detail.path;
|
|
||||||
const method = event.detail.verb;
|
|
||||||
|
|
||||||
console.log(`Request path: ${path}, method: ${method}`);
|
|
||||||
|
|
||||||
// Pattern match for notification service deletions (e.g., /admin/settings/notifications/123)
|
|
||||||
if (path && method === 'DELETE' && path.match(/^\/admin\/settings\/notifications\/\d+$/)) {
|
|
||||||
console.log("Detected notification service deletion request via URL pattern");
|
|
||||||
|
|
||||||
// This is definitely a delete request - store this information
|
|
||||||
window.isServiceDeleteRequest = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('htmx:afterRequest', function(event) {
|
|
||||||
// Check for notification service deletion multiple ways
|
|
||||||
const isDeleteRequest =
|
|
||||||
// Check global flag from the triggerServiceDelete function
|
|
||||||
window.currentlyDeletingService ||
|
|
||||||
// Check flag from beforeRequest handler
|
|
||||||
window.isServiceDeleteRequest ||
|
|
||||||
// Check URL pattern directly from this event
|
|
||||||
(event.detail.pathInfo &&
|
|
||||||
event.detail.pathInfo.requestPath &&
|
|
||||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
|
||||||
event.detail.verb === 'DELETE');
|
|
||||||
|
|
||||||
console.log(`Is delete request: ${isDeleteRequest}`);
|
|
||||||
|
|
||||||
// If this is a successful delete request, show notification
|
|
||||||
if (isDeleteRequest && event.detail.successful) {
|
|
||||||
console.log("Delete request was successful");
|
|
||||||
|
|
||||||
let serviceName = "Unknown";
|
|
||||||
|
|
||||||
// Try multiple sources for service name
|
|
||||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
|
||||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
|
||||||
// Fallback to our stored service info
|
|
||||||
serviceName = window.lastDeletedService.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Showing success notification for deleted service: ${serviceName}`);
|
|
||||||
showToast(`Notification service "${serviceName}" deleted successfully`, 'success');
|
|
||||||
|
|
||||||
// Clear flags
|
|
||||||
window.currentlyDeletingService = false;
|
|
||||||
window.isServiceDeleteRequest = false;
|
|
||||||
window.lastDeletedService = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('htmx:responseError', function(event) {
|
|
||||||
console.log("HTMX response error:", event.detail);
|
|
||||||
|
|
||||||
// Similar logic as success but for errors
|
|
||||||
const isDeleteRequest =
|
|
||||||
window.currentlyDeletingService ||
|
|
||||||
window.isServiceDeleteRequest ||
|
|
||||||
(event.detail.pathInfo &&
|
|
||||||
event.detail.pathInfo.requestPath &&
|
|
||||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
|
||||||
event.detail.verb === 'DELETE');
|
|
||||||
|
|
||||||
let errorMsg = 'An error occurred';
|
|
||||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
|
||||||
errorMsg = event.detail.xhr.responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDeleteRequest) {
|
|
||||||
console.log("Delete request failed");
|
|
||||||
|
|
||||||
let serviceName = "Unknown";
|
|
||||||
|
|
||||||
// Try multiple sources for service name
|
|
||||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
|
||||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
|
||||||
// Fallback to our stored service info
|
|
||||||
serviceName = window.lastDeletedService.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorMsg = `Failed to delete notification service "${serviceName}"`;
|
|
||||||
|
|
||||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
|
||||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Showing error notification: ${errorMsg}`);
|
|
||||||
showToast(errorMsg, 'error');
|
|
||||||
|
|
||||||
// Clear flags
|
|
||||||
window.currentlyDeletingService = false;
|
|
||||||
window.isServiceDeleteRequest = false;
|
|
||||||
window.lastDeletedService = null;
|
|
||||||
} else {
|
|
||||||
showToast(errorMsg, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle modal hide buttons
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
|
||||||
hideButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const modalId = this.getAttribute('data-modal-hide');
|
|
||||||
const modal = document.getElementById(modalId);
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
modal.classList.remove('flex');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show any success or error messages as toasts
|
|
||||||
if (document.querySelector('.success-message')) {
|
|
||||||
const successMsg = document.querySelector('.success-message').textContent.trim();
|
|
||||||
if (successMsg) {
|
|
||||||
showToast(successMsg, 'success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.querySelector('.error-message')) {
|
|
||||||
const errorMsg = document.querySelector('.error-message').textContent.trim();
|
|
||||||
if (errorMsg) {
|
|
||||||
showToast(errorMsg, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="notifications-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
|
||||||
<div class="pb-8 w-full">
|
|
||||||
<!-- Success Message (hidden, used for HTMX responses) -->
|
|
||||||
if data.SuccessMessage != "" {
|
|
||||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
|
||||||
}
|
|
||||||
<!-- Error Message (hidden, used for HTMX responses) -->
|
|
||||||
if data.ErrorMessage != "" {
|
|
||||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
||||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
|
||||||
Notification Services
|
|
||||||
</h1>
|
|
||||||
<a href="/admin/settings/notifications/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
|
||||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
|
||||||
Add Notification Service
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- List of Notification Services -->
|
|
||||||
if len(data.NotificationServices) == 0 {
|
|
||||||
<div class="text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg">
|
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4">
|
|
||||||
<i class="fas fa-bell text-2xl text-blue-600 dark:text-blue-400"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No notification services configured</h3>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Add a notification service to receive alerts for job events.</p>
|
|
||||||
<a href="/admin/settings/notifications/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
|
||||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
|
||||||
Add First Notification Service
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
|
||||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
for _, service := range data.NotificationServices {
|
|
||||||
<li>
|
|
||||||
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
|
||||||
<div class="px-4 py-4 sm:px-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
if service.Type == "email" {
|
|
||||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3">
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
</div>
|
|
||||||
} else if service.Type == "webhook" {
|
|
||||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3">
|
|
||||||
<i class="fas fa-bell"></i>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
|
||||||
{ service.Name }
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
{ service.Description }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
|
||||||
<a
|
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID)) }
|
|
||||||
class="text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
|
||||||
>
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<!-- Add notification delete dialog -->
|
|
||||||
@NotificationDialog(
|
|
||||||
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
|
||||||
"Delete Notification Service",
|
|
||||||
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
|
||||||
"text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800",
|
|
||||||
"Delete",
|
|
||||||
"delete",
|
|
||||||
service.ID,
|
|
||||||
service.Name,
|
|
||||||
)
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={ showModal(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
|
|
||||||
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
|
||||||
>
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 sm:flex sm:justify-between">
|
|
||||||
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span
|
|
||||||
class={ "px-2 py-1 text-xs font-medium rounded-full",
|
|
||||||
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
|
||||||
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled) }
|
|
||||||
>
|
|
||||||
if service.IsEnabled {
|
|
||||||
Active
|
|
||||||
} else {
|
|
||||||
Disabled
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full">
|
|
||||||
{ service.Type }
|
|
||||||
</span>
|
|
||||||
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
|
||||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full">
|
|
||||||
{ fmt.Sprintf("%d triggers", len(service.EventTriggers)) }
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
|
||||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full">
|
|
||||||
{ fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount + service.FailureCount) }
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
if service.Type == "webhook" {
|
|
||||||
<div class="mt-2 md:mt-0 flex items-center space-x-4">
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="text-gray-500 dark:text-gray-400">Events:</span>
|
|
||||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
|
||||||
if len(service.EventTriggers) == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
for i, trigger := range service.EventTriggers {
|
|
||||||
if i > 0 {
|
|
||||||
<span>, </span>
|
|
||||||
}
|
|
||||||
{ trigger }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="text-gray-500 dark:text-gray-400">Retry:</span>
|
|
||||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
|
||||||
if service.RetryPolicy == "" {
|
|
||||||
Default
|
|
||||||
} else {
|
|
||||||
{ service.RetryPolicy }
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
|
||||||
<p>
|
|
||||||
Last sent:
|
|
||||||
if service.SuccessCount > 0 {
|
|
||||||
"Recently"
|
|
||||||
} else {
|
|
||||||
"Never"
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Help Notice -->
|
|
||||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
|
||||||
Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Set dark background color if in dark mode
|
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
|
||||||
document.getElementById('notifications-container').style.backgroundColor = '#111827';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listener for theme changes
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
document.getElementById('notifications-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,48 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
||||||
@FormScripts() // Include the form-specific scripts
|
|
||||||
@components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx) {
|
@components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx) {
|
||||||
|
<script>
|
||||||
|
// Toggle notification fields based on selection
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeSelector = document.getElementById('notification_type');
|
||||||
|
// Ensure typeSelector exists before adding listener
|
||||||
|
if (!typeSelector) {
|
||||||
|
console.warn("Notification type selector not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFields = document.querySelectorAll('.notification-fields');
|
||||||
|
const commonFields = document.querySelectorAll('.common-fields');
|
||||||
|
|
||||||
|
function toggleFields() {
|
||||||
|
// Hide all specific fields first
|
||||||
|
allFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Show/hide common fields based on selection
|
||||||
|
const selectedType = typeSelector.value;
|
||||||
|
if (selectedType) {
|
||||||
|
// Show common fields (name, description, is_enabled, submit)
|
||||||
|
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||||
|
|
||||||
|
// Show the selected type's specific fields
|
||||||
|
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||||
|
if (fieldsToShow) {
|
||||||
|
fieldsToShow.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide common fields if no type selected
|
||||||
|
commonFields.forEach(field => field.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelector.addEventListener('change', toggleFields);
|
||||||
|
|
||||||
|
// Initialize form state on load (if editing or if a type is pre-selected)
|
||||||
|
toggleFields();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||||
|
|
||||||
<div id="notification-form-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
<div id="notification-form-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||||
@@ -41,10 +81,12 @@ templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
|||||||
<form id="notification-form"
|
<form id="notification-form"
|
||||||
if data.IsNew {
|
if data.IsNew {
|
||||||
hx-post="/admin/settings/notifications"
|
hx-post="/admin/settings/notifications"
|
||||||
|
hx-redirect="/admin/settings/notifications"
|
||||||
} else {
|
} else {
|
||||||
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||||
|
hx-redirect="/admin/settings/notifications"
|
||||||
}
|
}
|
||||||
hx-target="#notification-form-container">
|
hx-target="body">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||||
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
<select id="notification_type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
|||||||
@@ -182,57 +182,167 @@ templ RcloneCommandOptions(currentCommandID uint) { // Accept currentCommandID
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ SourceSelection() {
|
templ SourceSelection(providers []db.StorageProvider) {
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label for="source_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source Type</label>
|
<label for="source_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source Type</label>
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
<!-- Provider selection -->
|
||||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<input id="use_source_provider" name="use_source_provider" type="checkbox" x-model="useSourceProvider" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<label for="use_source_provider" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Use existing storage provider</label>
|
||||||
</div>
|
</div>
|
||||||
<select id="source_type" name="source_type" x-model="sourceType"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
<template x-if="useSourceProvider">
|
||||||
<option value="local">Local</option>
|
<div class="mt-2 space-y-4">
|
||||||
<option value="sftp">SFTP</option>
|
<div>
|
||||||
<option value="ftp">FTP</option>
|
<label for="source_provider_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Select provider</label>
|
||||||
<option value="s3">S3</option>
|
<div class="relative">
|
||||||
<option value="b2">Backblaze B2</option>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<option value="wasabi">Wasabi</option>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
<option value="minio">MinIO</option>
|
</div>
|
||||||
<option value="smb">SMB</option>
|
<select
|
||||||
<option value="nextcloud">NextCloud</option>
|
id="source_provider_id"
|
||||||
<option value="webdav">WebDAV</option>
|
name="source_provider_id"
|
||||||
<option value="gdrive">Google Drive (BETA)</option>
|
x-model="sourceProviderId"
|
||||||
<option value="gphotos">Google Photos (BETA)</option>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<option value="hetzner">Hetzner Storage Box</option>
|
hx-get="/api/storage-providers/options"
|
||||||
</select>
|
hx-trigger="load delay:500ms"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<option value="">Select a provider...</option>
|
||||||
|
for _, provider := range providers {
|
||||||
|
<option value={ fmt.Sprintf("%d", provider.ID) }>{ provider.Name } ({ string(provider.Type) })</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="/storage-providers/new" target="_blank" class="text-blue-600 hover:underline flex items-center text-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i> Add new storage provider
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="text-sm text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-3 py-1.5 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
|
||||||
|
hx-get="/api/storage-providers/options"
|
||||||
|
hx-target="#source_provider_id"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<i class="fas fa-sync-alt mr-1"></i> Refresh List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual configuration -->
|
||||||
|
<template x-if="!useSourceProvider">
|
||||||
|
<div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<select id="source_type" name="source_type" x-model="sourceType"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="local">Local</option>
|
||||||
|
<option value="sftp">SFTP</option>
|
||||||
|
<option value="ftp">FTP</option>
|
||||||
|
<option value="s3">S3</option>
|
||||||
|
<option value="b2">Backblaze B2</option>
|
||||||
|
<option value="wasabi">Wasabi</option>
|
||||||
|
<option value="minio">MinIO</option>
|
||||||
|
<option value="smb">SMB</option>
|
||||||
|
<option value="nextcloud">NextCloud</option>
|
||||||
|
<option value="webdav">WebDAV</option>
|
||||||
|
<option value="drive">Google Drive (BETA)</option>
|
||||||
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
|
<option value="hetzner">Hetzner Storage Box</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ DestinationSelection() {
|
templ DestinationSelection(providers []db.StorageProvider) {
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label for="destination_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Destination Type</label>
|
<label for="destination_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Destination Type</label>
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
<!-- Provider selection -->
|
||||||
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<input id="use_destination_provider" name="use_destination_provider" type="checkbox" x-model="useDestinationProvider" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<label for="use_destination_provider" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Use existing storage provider</label>
|
||||||
</div>
|
</div>
|
||||||
<select id="destination_type" name="destination_type" x-model="destinationType"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
<template x-if="useDestinationProvider">
|
||||||
<option value="local">Local</option>
|
<div class="mt-2 space-y-4">
|
||||||
<option value="sftp">SFTP</option>
|
<div>
|
||||||
<option value="ftp">FTP</option>
|
<label for="destination_provider_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Select provider</label>
|
||||||
<option value="s3">S3</option>
|
<div class="relative">
|
||||||
<option value="b2">Backblaze B2</option>
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
<option value="wasabi">Wasabi</option>
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
<option value="minio">MinIO</option>
|
</div>
|
||||||
<option value="smb">SMB</option>
|
<select
|
||||||
<option value="nextcloud">NextCloud</option>
|
id="destination_provider_id"
|
||||||
<option value="webdav">WebDAV</option>
|
name="destination_provider_id"
|
||||||
<option value="gdrive">Google Drive (BETA)</option>
|
x-model="destinationProviderId"
|
||||||
<option value="gphotos">Google Photos (BETA)</option>
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<option value="hetzner">Hetzner Storage Box</option>
|
hx-get="/api/storage-providers/options"
|
||||||
</select>
|
hx-trigger="load delay:500ms"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<option value="">Select a provider...</option>
|
||||||
|
for _, provider := range providers {
|
||||||
|
<option value={ fmt.Sprintf("%d", provider.ID) }>{ provider.Name } ({ string(provider.Type) })</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="/storage-providers/new" target="_blank" class="text-blue-600 hover:underline flex items-center text-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i> Add new storage provider
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="text-sm text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-3 py-1.5 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
|
||||||
|
hx-get="/api/storage-providers/options"
|
||||||
|
hx-target="#destination_provider_id"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<i class="fas fa-sync-alt mr-1"></i> Refresh List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual configuration -->
|
||||||
|
<template x-if="!useDestinationProvider">
|
||||||
|
<div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||||
|
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<select id="destination_type" name="destination_type" x-model="destinationType"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option value="local">Local</option>
|
||||||
|
<option value="sftp">SFTP</option>
|
||||||
|
<option value="ftp">FTP</option>
|
||||||
|
<option value="s3">S3</option>
|
||||||
|
<option value="b2">Backblaze B2</option>
|
||||||
|
<option value="wasabi">Wasabi</option>
|
||||||
|
<option value="minio">MinIO</option>
|
||||||
|
<option value="smb">SMB</option>
|
||||||
|
<option value="nextcloud">NextCloud</option>
|
||||||
|
<option value="webdav">WebDAV</option>
|
||||||
|
<option value="drive">Google Drive (BETA)</option>
|
||||||
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
|
<option value="hetzner">Hetzner Storage Box</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
package providers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/common"
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/source"
|
|
||||||
"github.com/starfleetcptn/gomft/components/providers/destination"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns the form ID based on the form type and whether it's a source or destination
|
|
||||||
func formID(formType string, isSource bool) string {
|
|
||||||
if isSource {
|
|
||||||
return "source_config_form"
|
|
||||||
}
|
|
||||||
return "destination_config_form"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a user-friendly display name for the provider
|
|
||||||
func providerDisplayName(provider string) string {
|
|
||||||
switch provider {
|
|
||||||
case "sftp":
|
|
||||||
return "SFTP"
|
|
||||||
case "local":
|
|
||||||
return "Local Filesystem"
|
|
||||||
case "s3":
|
|
||||||
return "Amazon S3"
|
|
||||||
case "ftp":
|
|
||||||
return "FTP"
|
|
||||||
case "azure":
|
|
||||||
return "Azure Blob Storage"
|
|
||||||
default:
|
|
||||||
return strings.Title(provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templ ProviderForm(formType string, providers []string, isSource bool) {
|
|
||||||
<form
|
|
||||||
id={formID(formType, isSource)}
|
|
||||||
x-data={fmt.Sprintf("{ %sProvider: '', showAdvanced: false }", formType)}
|
|
||||||
class="space-y-8">
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-y-6 gap-x-4">
|
|
||||||
@common.NameField()
|
|
||||||
|
|
||||||
<div class="sm:col-span-4">
|
|
||||||
<label for={fmt.Sprintf("%s_provider", formType)} class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Provider Type</label>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
id={fmt.Sprintf("%s_provider", formType)}
|
|
||||||
name={fmt.Sprintf("%s_provider", formType)}
|
|
||||||
x-model={fmt.Sprintf("%sProvider", formType)}
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
<option value="" disabled selected>Select provider type</option>
|
|
||||||
for _, provider := range providers {
|
|
||||||
<option value={provider}>{providerDisplayName(provider)}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'sftp'", formType)}>
|
|
||||||
if isSource {
|
|
||||||
@source.SFTPSourceForm()
|
|
||||||
} else {
|
|
||||||
@destination.SFTPDestinationForm()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'local'", formType)}>
|
|
||||||
if isSource {
|
|
||||||
@source.LocalSourceForm()
|
|
||||||
} else {
|
|
||||||
@destination.LocalDestinationForm()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 's3'", formType)}>
|
|
||||||
if isSource {
|
|
||||||
@source.S3SourceForm()
|
|
||||||
} else {
|
|
||||||
@destination.S3DestinationForm()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'ftp'", formType)}>
|
|
||||||
if isSource {
|
|
||||||
@source.FTPSourceForm()
|
|
||||||
} else {
|
|
||||||
@destination.FTPDestinationForm()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-12" x-show={fmt.Sprintf("%sProvider", formType)}>
|
|
||||||
<div class="mt-6">
|
|
||||||
<label for="show_advanced" class="flex items-center cursor-pointer">
|
|
||||||
<div class="relative">
|
|
||||||
<input id="show_advanced" type="checkbox" x-model="showAdvanced" class="sr-only" />
|
|
||||||
<div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
|
|
||||||
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"
|
|
||||||
:class="showAdvanced ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-gray-700 font-medium">
|
|
||||||
Show Advanced Options
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="showAdvanced">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-y-6 gap-x-4 mt-6">
|
|
||||||
@common.FilePatternFields()
|
|
||||||
if isSource {
|
|
||||||
@common.ArchiveOptions()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
script formAlpineInit() {
|
|
||||||
return {
|
|
||||||
initProviderForm() {
|
|
||||||
// Initialize with values if editing existing config
|
|
||||||
if (window.editData && window.editData.configs) {
|
|
||||||
const config = window.editData.configs.find(c =>
|
|
||||||
isSource ? (c.id === window.editData.source_config_id) : (c.id === window.editData.destination_config_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
this[formType + 'Provider'] = config.provider;
|
|
||||||
this.name = config.name;
|
|
||||||
|
|
||||||
// Provider-specific fields
|
|
||||||
if (config.provider === 'sftp') {
|
|
||||||
this.host = config.host;
|
|
||||||
this.port = config.port;
|
|
||||||
this.username = config.username;
|
|
||||||
this.path = config.path;
|
|
||||||
|
|
||||||
if (config.key_file && config.key_file !== '') {
|
|
||||||
this.authType = 'key_file';
|
|
||||||
this.keyFile = config.key_file;
|
|
||||||
} else {
|
|
||||||
this.authType = 'password';
|
|
||||||
// Password is not included in edit data for security
|
|
||||||
}
|
|
||||||
} else if (config.provider === 'local') {
|
|
||||||
this.path = config.path;
|
|
||||||
} else if (config.provider === 's3') {
|
|
||||||
this.bucket = config.bucket;
|
|
||||||
this.region = config.region;
|
|
||||||
this.path = config.path;
|
|
||||||
this.accessKey = config.access_key;
|
|
||||||
|
|
||||||
if (config.endpoint && config.endpoint !== '') {
|
|
||||||
this.useCustomEndpoint = true;
|
|
||||||
this.endpoint = config.endpoint;
|
|
||||||
} else {
|
|
||||||
this.useCustomEndpoint = false;
|
|
||||||
}
|
|
||||||
} else if (config.provider === 'ftp') {
|
|
||||||
this.host = config.host;
|
|
||||||
this.port = config.port;
|
|
||||||
this.username = config.username;
|
|
||||||
this.path = config.path;
|
|
||||||
this.useFTPS = config.use_ftps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced options
|
|
||||||
if (config.include_pattern) this.filePattern = config.include_pattern;
|
|
||||||
if (config.exclude_pattern) this.excludePattern = config.exclude_pattern;
|
|
||||||
|
|
||||||
if (isSource && config.extract_archives) {
|
|
||||||
this.extractArchives = true;
|
|
||||||
this.deleteArchives = config.delete_archives;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
providerChanged() {
|
|
||||||
console.log("Provider changed to: " + this[formType + 'Provider']);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
type RcloneImportPreview struct {
|
||||||
|
Remotes []RcloneRemotePreview
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RcloneRemotePreview struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Fields map[string]string
|
||||||
|
Import bool // Should import
|
||||||
|
}
|
||||||
@@ -3,7 +3,28 @@ package toast
|
|||||||
templ ShowToastJS() {
|
templ ShowToastJS() {
|
||||||
<script>
|
<script>
|
||||||
// Notification system
|
// Notification system
|
||||||
|
// Global tracking of shown messages to prevent duplicates
|
||||||
|
window.shownToastMessages = window.shownToastMessages || [];
|
||||||
|
|
||||||
function showToast(message, type) {
|
function showToast(message, type) {
|
||||||
|
// Check if this exact message has been shown in the last 500ms
|
||||||
|
const messageKey = `${message}-${type}`;
|
||||||
|
if (window.shownToastMessages.includes(messageKey)) {
|
||||||
|
console.log(`Preventing duplicate toast: ${messageKey}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to shown messages
|
||||||
|
window.shownToastMessages.push(messageKey);
|
||||||
|
|
||||||
|
// Remove from tracking after 500ms to allow the same message later if needed
|
||||||
|
setTimeout(() => {
|
||||||
|
const index = window.shownToastMessages.indexOf(messageKey);
|
||||||
|
if (index > -1) {
|
||||||
|
window.shownToastMessages.splice(index, 1);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
const toastContainer = document.getElementById('toast-container');
|
const toastContainer = document.getElementById('toast-container');
|
||||||
if (!toastContainer) {
|
if (!toastContainer) {
|
||||||
console.error("Toast container not found!");
|
console.error("Toast container not found!");
|
||||||
|
|||||||
@@ -0,0 +1,954 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageProviderFormData struct {
|
||||||
|
Provider *db.StorageProvider
|
||||||
|
IsEdit bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTitle returns the appropriate title based on whether we're editing or creating
|
||||||
|
func getTitle(isEdit bool) string {
|
||||||
|
if isEdit {
|
||||||
|
return "Edit Storage Provider"
|
||||||
|
}
|
||||||
|
return "New Storage Provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main template for Storage Provider form
|
||||||
|
templ StorageProviderForm(ctx context.Context, data StorageProviderFormData) {
|
||||||
|
@LayoutWithContext(getTitle(data.IsEdit), ctx) {
|
||||||
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-server w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
if data.IsEdit {
|
||||||
|
Edit Storage Provider
|
||||||
|
} else {
|
||||||
|
New Storage Provider
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
<a href="/storage-providers" class="text-blue-600 dark:text-blue-400 hover:underline flex items-center">
|
||||||
|
<i class="fas fa-arrow-left mr-1.5"></i>
|
||||||
|
Back to Providers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if data.Error != "" {
|
||||||
|
<div class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
<span>{ data.Error }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
if data.IsEdit {
|
||||||
|
<form action={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d", data.Provider.ID)) } method="POST">
|
||||||
|
<input type="hidden" name="_method" value="PUT" />
|
||||||
|
@formFields(data)
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<form action="/storage-providers" method="POST">
|
||||||
|
@formFields(data)
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Include the provider form scripts -->
|
||||||
|
@providerFormScript()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form fields template
|
||||||
|
templ formFields(data StorageProviderFormData) {
|
||||||
|
<!-- Basic Information -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Basic Information</h2>
|
||||||
|
|
||||||
|
<!-- Provider Name -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Provider Name <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="name" name="name" value={ data.Provider.Name } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Storage Provider Name" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Provider Type -->
|
||||||
|
<div>
|
||||||
|
<label for="type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Provider Type <span class="text-red-500">*</span></label>
|
||||||
|
<select id="type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required onchange="toggleProviderFields()">
|
||||||
|
<option value="" disabled
|
||||||
|
if data.Provider.Type == "" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select provider type
|
||||||
|
</option>
|
||||||
|
<!-- Server-based providers -->
|
||||||
|
<optgroup label="Server-based">
|
||||||
|
<option value="sftp"
|
||||||
|
if data.Provider.Type == db.ProviderTypeSFTP {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>SFTP</option>
|
||||||
|
<option value="hetzner"
|
||||||
|
if data.Provider.Type == db.ProviderTypeHetzner {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Hetzner Storage Box</option>
|
||||||
|
<option value="ftp"
|
||||||
|
if data.Provider.Type == db.ProviderTypeFTP {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>FTP</option>
|
||||||
|
<option value="smb"
|
||||||
|
if data.Provider.Type == db.ProviderTypeSMB {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>SMB/CIFS</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<!-- Object Storage -->
|
||||||
|
<optgroup label="Object Storage">
|
||||||
|
<option value="s3"
|
||||||
|
if data.Provider.Type == db.ProviderTypeS3 {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Amazon S3</option>
|
||||||
|
<option value="wasabi"
|
||||||
|
if data.Provider.Type == "wasabi" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Wasabi</option>
|
||||||
|
<option value="minio"
|
||||||
|
if data.Provider.Type == "minio" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>MinIO</option>
|
||||||
|
<option value="b2"
|
||||||
|
if data.Provider.Type == "b2" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Backblaze B2</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<!-- Web-based storage -->
|
||||||
|
<optgroup label="Web Storage">
|
||||||
|
<option value="webdav"
|
||||||
|
if data.Provider.Type == "webdav" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>WebDAV</option>
|
||||||
|
<option value="nextcloud"
|
||||||
|
if data.Provider.Type == "nextcloud" {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Nextcloud</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<!-- Cloud providers -->
|
||||||
|
<optgroup label="Cloud Storage">
|
||||||
|
<option value="onedrive"
|
||||||
|
if data.Provider.Type == db.ProviderTypeOneDrive {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>OneDrive</option>
|
||||||
|
<option value="drive"
|
||||||
|
if data.Provider.Type == db.ProviderTypeGoogleDrive {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Google Drive</option>
|
||||||
|
<option value="gphotos"
|
||||||
|
if data.Provider.Type == db.ProviderTypeGooglePhoto {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Google Photos</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<!-- Local -->
|
||||||
|
<optgroup label="Local">
|
||||||
|
<option value="local"
|
||||||
|
if data.Provider.Type == db.ProviderTypeLocal {
|
||||||
|
selected="selected"
|
||||||
|
}
|
||||||
|
>Local Filesystem</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connection Details - SFTP/FTP/SMB -->
|
||||||
|
<div id="sftp-ftp-fields" class="mb-6 provider-fields hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Connection Details</h2>
|
||||||
|
|
||||||
|
<!-- Host -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Host <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="host" name="host" value={ data.Provider.Host } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="e.g., sftp.example.com or 192.168.1.10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Port -->
|
||||||
|
<div id="port-field" class="mb-4">
|
||||||
|
<label for="port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||||
|
<input type="number" id="port" name="port" value={ fmt.Sprint(data.Provider.Port) } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="22" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty for default (SFTP: 22, FTP: 21, SMB: 445, WebDAV: 80/443)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Username -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="username" name="username" value={ data.Provider.Username } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Your login username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Your login password" />
|
||||||
|
if data.IsEdit {
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty to keep the current password</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Key File (SFTP only) -->
|
||||||
|
<div id="key-file-field" class="mb-4 hidden">
|
||||||
|
<label for="keyFile" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Key File Path</label>
|
||||||
|
<input type="text" id="keyFile" name="keyFile" value={ data.Provider.KeyFile } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="e.g., /home/user/.ssh/id_rsa" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Absolute path to private key file (if using key-based authentication)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Domain (SMB only) -->
|
||||||
|
<div id="domain-field" class="mb-4 hidden">
|
||||||
|
<label for="domain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Domain</label>
|
||||||
|
<input type="text" id="domain" name="domain" value={ data.Provider.Domain } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="e.g., WORKGROUP or domain.local" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passive Mode (FTP only) -->
|
||||||
|
<div id="passive-mode-field" class="mb-4 hidden">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="passiveMode" name="passiveMode" type="checkbox" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
if data.Provider.PassiveMode != nil && *data.Provider.PassiveMode {
|
||||||
|
checked="checked"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label for="passiveMode" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Use Passive Mode</label>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Recommended for most FTP connections through firewalls</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- S3 Fields -->
|
||||||
|
<div id="s3-fields" class="mb-6 provider-fields hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">S3 Connection Details</h2>
|
||||||
|
|
||||||
|
<!-- Endpoint -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Endpoint
|
||||||
|
<span id="endpoint-required" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="endpoint" name="endpoint" value={ data.Provider.Endpoint }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="e.g., s3.amazonaws.com, s3.us-west-1.wasabisys.com" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Custom endpoint URL (only needed for non-standard regions or non-AWS S3-compatible services). Optional for B2.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Region -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Region
|
||||||
|
<span id="region-required" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="region" name="region" value={ data.Provider.Region }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="e.g., us-east-1, eu-central-1" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
AWS Region where your S3 bucket is located (e.g., us-east-1, eu-west-1). Optional for B2 and Wasabi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bucket -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="bucket" name="bucket" value={ data.Provider.Bucket }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Your bucket name" />
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your S3 bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access Key -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="accessKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="accessKey" name="accessKey" value={ data.Provider.AccessKey }
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Your access key/key ID" />
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your AWS Access Key ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Secret Key -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key <span class="text-red-500">*</span></label>
|
||||||
|
<input type="password" id="secretKey" name="secretKey"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="Your secret access key" />
|
||||||
|
if data.IsEdit {
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty to keep the current secret key</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cloud Storage Fields (OneDrive, Google Drive, Google Photos) -->
|
||||||
|
<div id="cloud-fields" class="mb-6 provider-fields hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Cloud Storage Details</h2>
|
||||||
|
|
||||||
|
<!-- Client ID -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="clientID" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client ID <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="clientID" name="clientID" value={ data.Provider.ClientID } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="OAuth client ID from developer console" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client Secret -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="clientSecret" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Client Secret <span class="text-red-500">*</span></label>
|
||||||
|
<input type="password" id="clientSecret" name="clientSecret" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="OAuth client secret from developer console" />
|
||||||
|
if data.IsEdit {
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty to keep the current client secret</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Google Drive - Drive ID -->
|
||||||
|
<div id="drive-id-field" class="mb-4 hidden">
|
||||||
|
<label for="driveID" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Drive ID</label>
|
||||||
|
<input type="text" id="driveID" name="driveID" value={ data.Provider.DriveID } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="ID of shared/team drive (from Drive URL)" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Only required for shared drives</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Google Drive - Team Drive -->
|
||||||
|
<div id="team-drive-field" class="mb-4 hidden">
|
||||||
|
<label for="teamDrive" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Team Drive</label>
|
||||||
|
<input type="text" id="teamDrive" name="teamDrive" value={ data.Provider.TeamDrive } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Team drive identifier" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Only required for team drives</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Google Photos - Read Only -->
|
||||||
|
<div id="readonly-field" class="mb-4 hidden">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="readOnly" name="readOnly" type="checkbox" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
if data.Provider.ReadOnly != nil && *data.Provider.ReadOnly {
|
||||||
|
checked="checked"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label for="readOnly" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Read Only Mode</label>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Google Photos has limited write capabilities</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Local File System Fields -->
|
||||||
|
<div id="local-fields" class="mb-6 provider-fields hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Local File System</h2>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="localPath" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Base Path <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="localPath" name="localPath" value={ data.Provider.Host } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="e.g., /data/files or C:\transfer\data" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Absolute path on the server's file system</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Buttons -->
|
||||||
|
<div class="flex items-center justify-between mt-8">
|
||||||
|
<a href="/storage-providers" class="text-gray-500 bg-gray-50 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="submit" name="test" value="true" class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-700">
|
||||||
|
Save & Test
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
Save Provider
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden fields for S3 form data to ensure it gets submitted correctly -->
|
||||||
|
<input type="hidden" id="hidden_endpoint" name="endpoint" value={ data.Provider.Endpoint } />
|
||||||
|
<input type="hidden" id="hidden_region" name="region" value={ data.Provider.Region } />
|
||||||
|
<input type="hidden" id="hidden_bucket" name="bucket" value={ data.Provider.Bucket } />
|
||||||
|
<input type="hidden" id="hidden_accessKey" name="accessKey" value={ data.Provider.AccessKey } />
|
||||||
|
<input type="hidden" id="hidden_secretKey" name="secretKey" />
|
||||||
|
|
||||||
|
<!-- Hidden fields for Google Drive and Google Photos -->
|
||||||
|
<input type="hidden" id="hidden_clientID" name="clientID" value={ data.Provider.ClientID } />
|
||||||
|
<input type="hidden" id="hidden_clientSecret" name="clientSecret" />
|
||||||
|
}
|
||||||
|
|
||||||
|
// JavaScript helper for toggling provider fields
|
||||||
|
templ providerFormScript() {
|
||||||
|
<script>
|
||||||
|
// Set current active fields on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
toggleProviderFields();
|
||||||
|
|
||||||
|
// Add event listener to track if user manually changes the port
|
||||||
|
const portField = document.getElementById('port');
|
||||||
|
portField.addEventListener('input', function() {
|
||||||
|
// Mark the field as user-modified
|
||||||
|
this.dataset.userModified = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners for S3 fields
|
||||||
|
document.getElementById('endpoint').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_endpoint').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('region').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_region').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('bucket').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_bucket').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('accessKey').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_accessKey').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('secretKey').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_secretKey').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners for Google Drive/Photos fields
|
||||||
|
document.getElementById('clientID').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_clientID').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clientSecret').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_clientSecret').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add form submit listener to ensure all hidden fields are populated
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// Find active provider type
|
||||||
|
const providerType = document.getElementById('type').value;
|
||||||
|
|
||||||
|
// If S3-compatible, update hidden fields
|
||||||
|
if (['s3', 'wasabi', 'minio', 'b2'].includes(providerType)) {
|
||||||
|
document.getElementById('hidden_endpoint').value = document.getElementById('endpoint').value;
|
||||||
|
document.getElementById('hidden_region').value = document.getElementById('region').value;
|
||||||
|
document.getElementById('hidden_bucket').value = document.getElementById('bucket').value;
|
||||||
|
document.getElementById('hidden_accessKey').value = document.getElementById('accessKey').value;
|
||||||
|
|
||||||
|
// Make sure secretKey is always copied to the hidden field
|
||||||
|
// This is especially important for B2 which uses this as Application Key
|
||||||
|
const secretKeyValue = document.getElementById('secretKey').value;
|
||||||
|
document.getElementById('hidden_secretKey').value = secretKeyValue;
|
||||||
|
|
||||||
|
// For validation - ensure we have appropriate fields for each provider type
|
||||||
|
if (providerType === 'b2') {
|
||||||
|
// B2 does not require region or endpoint
|
||||||
|
if (!document.getElementById('hidden_bucket').value) {
|
||||||
|
alert('Bucket name is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_accessKey').value) {
|
||||||
|
alert('Account ID is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!secretKeyValue && !document.getElementById('hidden_secretKey').value) {
|
||||||
|
alert('Application Key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (providerType === 'wasabi') {
|
||||||
|
// Wasabi does not require region
|
||||||
|
if (!document.getElementById('hidden_endpoint').value) {
|
||||||
|
alert('Endpoint is required for Wasabi');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_bucket').value) {
|
||||||
|
alert('Bucket name is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_accessKey').value) {
|
||||||
|
alert('Access key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!secretKeyValue && !document.getElementById('hidden_secretKey').value) {
|
||||||
|
alert('Secret key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Submitting S3-compatible form with:', {
|
||||||
|
provider: providerType,
|
||||||
|
endpoint: document.getElementById('hidden_endpoint').value,
|
||||||
|
region: document.getElementById('hidden_region').value,
|
||||||
|
bucket: document.getElementById('hidden_bucket').value,
|
||||||
|
accessKey: document.getElementById('hidden_accessKey').value,
|
||||||
|
secretKey: document.getElementById('hidden_secretKey').value ? '[PRESENT]' : '[EMPTY]',
|
||||||
|
secretKeyLength: document.getElementById('hidden_secretKey').value.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Google Drive or Google Photos, update hidden fields
|
||||||
|
if (['drive', 'gphotos', 'onedrive'].includes(providerType)) {
|
||||||
|
document.getElementById('hidden_clientID').value = document.getElementById('clientID').value;
|
||||||
|
|
||||||
|
// Make sure clientSecret is always copied to the hidden field
|
||||||
|
const clientSecretValue = document.getElementById('clientSecret').value;
|
||||||
|
document.getElementById('hidden_clientSecret').value = clientSecretValue;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!document.getElementById('hidden_clientID').value) {
|
||||||
|
alert('Client ID is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Submitting cloud storage form with:', {
|
||||||
|
provider: providerType,
|
||||||
|
clientID: document.getElementById('hidden_clientID').value,
|
||||||
|
clientSecret: document.getElementById('hidden_clientSecret').value ? '[PRESENT]' : '[EMPTY]',
|
||||||
|
clientSecretLength: document.getElementById('hidden_clientSecret').value.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug WebDAV form submission
|
||||||
|
if (['webdav', 'nextcloud'].includes(providerType)) {
|
||||||
|
console.log('Submitting WebDAV form with:', {
|
||||||
|
provider: providerType,
|
||||||
|
host: document.getElementById('host').value,
|
||||||
|
username: document.getElementById('username').value,
|
||||||
|
password: document.getElementById('password').value ? '[PRESENT]' : '[EMPTY]',
|
||||||
|
passwordLength: document.getElementById('password').value.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleProviderFields() {
|
||||||
|
const provider = document.getElementById('type').value;
|
||||||
|
|
||||||
|
// Hide all provider fields first
|
||||||
|
const providerFieldsets = document.querySelectorAll('.provider-fields');
|
||||||
|
providerFieldsets.forEach(fieldset => {
|
||||||
|
fieldset.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-populate port based on provider type
|
||||||
|
const portField = document.getElementById('port');
|
||||||
|
const isPortEmpty = portField.value === '';
|
||||||
|
const userModified = portField.dataset.userModified === 'true';
|
||||||
|
|
||||||
|
// Only set default port if field is empty or hasn't been modified by user
|
||||||
|
if (isPortEmpty || !userModified) {
|
||||||
|
if (provider === 'sftp') {
|
||||||
|
portField.value = '22';
|
||||||
|
} else if (provider === 'hetzner') {
|
||||||
|
portField.value = '23';
|
||||||
|
} else if (provider === 'ftp') {
|
||||||
|
portField.value = '21';
|
||||||
|
} else if (provider === 'smb') {
|
||||||
|
portField.value = '445';
|
||||||
|
} else if (provider === 'webdav' || provider === 'nextcloud') {
|
||||||
|
portField.value = '443';
|
||||||
|
} else {
|
||||||
|
portField.value = '';
|
||||||
|
}
|
||||||
|
// Reset user modified flag if we're setting it programmatically
|
||||||
|
portField.dataset.userModified = 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the appropriate fields based on the selected provider type
|
||||||
|
console.log("Provider type selected:", provider);
|
||||||
|
|
||||||
|
// Server-based connection fields (SFTP, FTP, Hetzner)
|
||||||
|
if (['sftp', 'ftp', 'hetzner'].includes(provider)) {
|
||||||
|
document.getElementById('sftp-ftp-fields').classList.remove('hidden');
|
||||||
|
|
||||||
|
// SFTP/Hetzner-specific fields
|
||||||
|
if (provider === 'sftp' || provider === 'hetzner') {
|
||||||
|
document.getElementById('key-file-field').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update placeholders for Hetzner
|
||||||
|
if (provider === 'hetzner') {
|
||||||
|
// Update host field
|
||||||
|
const hostField = document.getElementById('host');
|
||||||
|
if (hostField) {
|
||||||
|
hostField.placeholder = "uXXXXXX.your-storagebox.de";
|
||||||
|
|
||||||
|
// Update host label and description
|
||||||
|
const hostLabel = document.querySelector('label[for="host"]');
|
||||||
|
if (hostLabel) {
|
||||||
|
hostLabel.textContent = "Storage Box Host";
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostDescription = hostField.nextElementSibling;
|
||||||
|
if (hostDescription?.tagName === 'P') {
|
||||||
|
hostDescription.textContent = "Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update username field
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
if (usernameField) {
|
||||||
|
usernameField.placeholder = "uXXXXXX";
|
||||||
|
|
||||||
|
// Update username description
|
||||||
|
const usernameDescription = usernameField.nextElementSibling;
|
||||||
|
if (usernameDescription?.tagName === 'P') {
|
||||||
|
usernameDescription.textContent = "Your Hetzner Storage Box username (typically matches your Storage Box number)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update key file field
|
||||||
|
const keyFileField = document.getElementById('keyFile');
|
||||||
|
if (keyFileField) {
|
||||||
|
keyFileField.placeholder = "/path/to/id_rsa";
|
||||||
|
|
||||||
|
// Update key file description
|
||||||
|
const keyFileDescription = keyFileField.nextElementSibling;
|
||||||
|
if (keyFileDescription?.tagName === 'P') {
|
||||||
|
keyFileDescription.textContent = "Path to your SSH private key file for Hetzner Storage Box authentication";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update port field description
|
||||||
|
const portField = document.getElementById('port');
|
||||||
|
if (portField) {
|
||||||
|
const portDescription = portField.nextElementSibling;
|
||||||
|
if (portDescription?.tagName === 'P') {
|
||||||
|
portDescription.textContent = "Connection port for Hetzner Storage Box (default: 23)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FTP-specific fields
|
||||||
|
if (provider === 'ftp') {
|
||||||
|
document.getElementById('passive-mode-field').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebDAV-based fields
|
||||||
|
if (['webdav', 'nextcloud'].includes(provider)) {
|
||||||
|
document.getElementById('sftp-ftp-fields').classList.remove('hidden');
|
||||||
|
// Hide fields that WebDAV doesn't use
|
||||||
|
document.getElementById('port-field').classList.add('hidden');
|
||||||
|
|
||||||
|
// Update placeholders for WebDAV
|
||||||
|
const hostField = document.getElementById('host');
|
||||||
|
if (hostField) {
|
||||||
|
hostField.placeholder = "https://webdav.example.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update label for host field
|
||||||
|
const hostLabel = document.querySelector('label[for="host"]');
|
||||||
|
if (hostLabel) {
|
||||||
|
hostLabel.textContent = "WebDAV URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update description for host field
|
||||||
|
const hostDescription = hostField?.nextElementSibling;
|
||||||
|
if (hostDescription?.tagName === 'P') {
|
||||||
|
hostDescription.textContent = provider === 'webdav' ?
|
||||||
|
"Full URL to your WebDAV server including protocol (https://)" :
|
||||||
|
"Full URL to your Nextcloud WebDAV endpoint (e.g., https://nextcloud.example.com/remote.php/dav/files/username/)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update username field
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
if (usernameField) {
|
||||||
|
usernameField.placeholder = provider === 'nextcloud' ? "nextcloud_username" : "webdav_username";
|
||||||
|
|
||||||
|
// Update username description
|
||||||
|
const usernameDescription = usernameField.nextElementSibling;
|
||||||
|
if (usernameDescription?.tagName === 'P') {
|
||||||
|
usernameDescription.textContent = provider === 'nextcloud' ?
|
||||||
|
"Your Nextcloud username for authentication" :
|
||||||
|
"Your WebDAV username for authentication";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password field
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
if (passwordField) {
|
||||||
|
passwordField.name = "password"; // Ensure name is set correctly
|
||||||
|
|
||||||
|
// Update password description
|
||||||
|
const passwordDescription = passwordField.nextElementSibling;
|
||||||
|
if (passwordDescription?.tagName === 'P') {
|
||||||
|
// Check if we're in edit mode
|
||||||
|
const editMode = passwordDescription.textContent.includes("Leave empty to keep");
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
// Edit mode - tell user they can leave password empty to keep current one
|
||||||
|
passwordDescription.textContent = provider === 'nextcloud' ?
|
||||||
|
"Leave empty to keep the current Nextcloud password" :
|
||||||
|
"Leave empty to keep the current WebDAV password";
|
||||||
|
} else {
|
||||||
|
// New provider - show regular password help text
|
||||||
|
passwordDescription.textContent = provider === 'nextcloud' ?
|
||||||
|
"Your Nextcloud password or app-specific password" :
|
||||||
|
"Your WebDAV password for authentication";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('key-file-field')) {
|
||||||
|
document.getElementById('key-file-field').classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (document.getElementById('domain-field')) {
|
||||||
|
document.getElementById('domain-field').classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset placeholders for other providers
|
||||||
|
const hostField = document.getElementById('host');
|
||||||
|
if (hostField && ['sftp', 'ftp', 'smb'].includes(provider)) {
|
||||||
|
hostField.placeholder = provider === 'sftp' ? "sftp.example.com" :
|
||||||
|
provider === 'ftp' ? "ftp.example.com" :
|
||||||
|
"192.168.1.10";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset label for host field
|
||||||
|
const hostLabel = document.querySelector('label[for="host"]');
|
||||||
|
if (hostLabel) {
|
||||||
|
hostLabel.textContent = "Host";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset username field
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
if (usernameField) {
|
||||||
|
usernameField.placeholder = provider === 'sftp' ? "sftp_username" :
|
||||||
|
provider === 'ftp' ? "ftp_username" :
|
||||||
|
provider === 'smb' ? "smb_username" : "username";
|
||||||
|
|
||||||
|
// Reset username description
|
||||||
|
const usernameDescription = usernameField.nextElementSibling;
|
||||||
|
if (usernameDescription?.tagName === 'P') {
|
||||||
|
usernameDescription.textContent = "Your login username";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset password field
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
if (passwordField) {
|
||||||
|
// Reset password description
|
||||||
|
const passwordDescription = passwordField.nextElementSibling;
|
||||||
|
if (passwordDescription?.tagName === 'P') {
|
||||||
|
// Check if we're in edit mode
|
||||||
|
const editMode = passwordDescription.textContent.includes("Leave empty to keep");
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
// Edit mode - tell user they can leave password empty to keep current one
|
||||||
|
if (provider === 'sftp') {
|
||||||
|
passwordDescription.textContent = "Leave empty to keep the current SFTP password";
|
||||||
|
} else if (provider === 'ftp') {
|
||||||
|
passwordDescription.textContent = "Leave empty to keep the current FTP password";
|
||||||
|
} else if (provider === 'smb') {
|
||||||
|
passwordDescription.textContent = "Leave empty to keep the current SMB password";
|
||||||
|
} else if (provider === 'hetzner') {
|
||||||
|
passwordDescription.textContent = "Leave empty to keep the current Hetzner Storage Box password";
|
||||||
|
} else {
|
||||||
|
passwordDescription.textContent = "Leave empty to keep the current password";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New provider - show regular password help text
|
||||||
|
if (provider === 'sftp') {
|
||||||
|
passwordDescription.textContent = "Your SFTP server password";
|
||||||
|
} else if (provider === 'ftp') {
|
||||||
|
passwordDescription.textContent = "Your FTP server password";
|
||||||
|
} else if (provider === 'smb') {
|
||||||
|
passwordDescription.textContent = "Your SMB/CIFS share password";
|
||||||
|
} else if (provider === 'hetzner') {
|
||||||
|
passwordDescription.textContent = "Your Hetzner Storage Box password";
|
||||||
|
} else {
|
||||||
|
passwordDescription.textContent = "Your login password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMB-specific fields
|
||||||
|
if (provider === 'smb') {
|
||||||
|
document.getElementById('sftp-ftp-fields').classList.remove('hidden');
|
||||||
|
document.getElementById('domain-field').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3-compatible storage
|
||||||
|
if (['s3', 'wasabi', 'minio', 'b2'].includes(provider)) {
|
||||||
|
document.getElementById('s3-fields').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Set region and endpoint requirements based on provider type
|
||||||
|
const regionRequired = document.getElementById('region-required');
|
||||||
|
const endpointRequired = document.getElementById('endpoint-required');
|
||||||
|
const regionInput = document.getElementById('region');
|
||||||
|
const endpointInput = document.getElementById('endpoint');
|
||||||
|
|
||||||
|
// Update Secret Key field description based on provider type
|
||||||
|
const secretKeyField = document.getElementById('secretKey');
|
||||||
|
if (secretKeyField) {
|
||||||
|
const secretKeyDescription = secretKeyField.nextElementSibling;
|
||||||
|
if (secretKeyDescription?.tagName === 'P') {
|
||||||
|
// Check if in edit mode
|
||||||
|
const editMode = secretKeyDescription.textContent.includes("Leave empty to keep");
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
// Edit mode - provider specific text
|
||||||
|
if (provider === 'b2') {
|
||||||
|
secretKeyDescription.textContent = "Leave empty to keep the current B2 Application Key";
|
||||||
|
} else if (provider === 'wasabi') {
|
||||||
|
secretKeyDescription.textContent = "Leave empty to keep the current Wasabi Secret Key";
|
||||||
|
} else if (provider === 'minio') {
|
||||||
|
secretKeyDescription.textContent = "Leave empty to keep the current MinIO Secret Key";
|
||||||
|
} else {
|
||||||
|
secretKeyDescription.textContent = "Leave empty to keep the current AWS Secret Access Key";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New provider - provider specific text
|
||||||
|
if (provider === 'b2') {
|
||||||
|
secretKeyDescription.textContent = "Your Backblaze B2 Application Key";
|
||||||
|
} else if (provider === 'wasabi') {
|
||||||
|
secretKeyDescription.textContent = "Your Wasabi Secret Key";
|
||||||
|
} else if (provider === 'minio') {
|
||||||
|
secretKeyDescription.textContent = "Your MinIO Secret Key";
|
||||||
|
} else {
|
||||||
|
secretKeyDescription.textContent = "Your AWS Secret Access Key";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For B2: endpoint and region are optional
|
||||||
|
if (provider === 'b2') {
|
||||||
|
regionRequired.style.display = 'none';
|
||||||
|
endpointRequired.style.display = 'none';
|
||||||
|
regionInput.removeAttribute('required');
|
||||||
|
endpointInput.removeAttribute('required');
|
||||||
|
}
|
||||||
|
// For Wasabi: region is optional
|
||||||
|
else if (provider === 'wasabi') {
|
||||||
|
regionRequired.style.display = 'none';
|
||||||
|
endpointRequired.style.display = 'inline';
|
||||||
|
regionInput.removeAttribute('required');
|
||||||
|
endpointInput.setAttribute('required', 'required');
|
||||||
|
}
|
||||||
|
// For S3 and MinIO: both are required
|
||||||
|
else {
|
||||||
|
regionRequired.style.display = 'inline';
|
||||||
|
endpointRequired.style.display = 'inline';
|
||||||
|
regionInput.setAttribute('required', 'required');
|
||||||
|
endpointInput.setAttribute('required', 'required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google services
|
||||||
|
if (['drive', 'gphotos'].includes(provider)) {
|
||||||
|
document.getElementById('cloud-fields').classList.remove('hidden');
|
||||||
|
document.getElementById('built-in-auth-field').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update client secret field description
|
||||||
|
const clientSecretField = document.getElementById('clientSecret');
|
||||||
|
if (clientSecretField) {
|
||||||
|
const clientSecretDescription = clientSecretField.nextElementSibling;
|
||||||
|
if (clientSecretDescription?.tagName === 'P') {
|
||||||
|
// Check if in edit mode
|
||||||
|
const editMode = clientSecretDescription.textContent.includes("Leave empty to keep");
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
// Edit mode - provider specific text
|
||||||
|
if (provider === 'drive') {
|
||||||
|
clientSecretDescription.textContent = "Leave empty to keep the current Google Drive client secret";
|
||||||
|
} else if (provider === 'gphotos') {
|
||||||
|
clientSecretDescription.textContent = "Leave empty to keep the current Google Photos client secret";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Drive specific fields
|
||||||
|
if (provider === 'drive') {
|
||||||
|
document.getElementById('drive-id-field').classList.remove('hidden');
|
||||||
|
document.getElementById('team-drive-field').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Photos specific fields
|
||||||
|
if (provider === 'gphotos') {
|
||||||
|
document.getElementById('gphotos-options-field').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneDrive
|
||||||
|
if (provider === 'onedrive') {
|
||||||
|
document.getElementById('cloud-fields').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update client secret field description
|
||||||
|
const clientSecretField = document.getElementById('clientSecret');
|
||||||
|
if (clientSecretField) {
|
||||||
|
const clientSecretDescription = clientSecretField.nextElementSibling;
|
||||||
|
if (clientSecretDescription?.tagName === 'P') {
|
||||||
|
// Check if in edit mode
|
||||||
|
const editMode = clientSecretDescription.textContent.includes("Leave empty to keep");
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
clientSecretDescription.textContent = "Leave empty to keep the current OneDrive client secret";
|
||||||
|
} else {
|
||||||
|
clientSecretDescription.textContent = "Your Microsoft Azure OAuth client secret";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local filesystem fields
|
||||||
|
if (provider === 'local') {
|
||||||
|
document.getElementById('local-fields').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test result dialog
|
||||||
|
templ ConnectionTestResult(success bool, message string, errorCode string) {
|
||||||
|
<div class="text-center">
|
||||||
|
if success {
|
||||||
|
<i class="fas fa-check-circle text-green-500 text-5xl mb-4"></i>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-green-500 dark:text-green-400">Connection Successful</h3>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-times-circle text-red-500 text-5xl mb-4"></i>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-red-500 dark:text-red-400">Connection Failed</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
{ message }
|
||||||
|
</p>
|
||||||
|
|
||||||
|
if errorCode != "" {
|
||||||
|
<div class="text-sm bg-gray-100 dark:bg-gray-800 p-2 rounded">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Error code: { errorCode }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StorageProviderGDriveHeadlessAuthData contains data needed for rendering the headless auth page for storage providers
|
||||||
|
type StorageProviderGDriveHeadlessAuthData struct {
|
||||||
|
AuthCommand string
|
||||||
|
ProviderID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageProviderGDriveHeadlessAuth renders the headless authentication page for Google Drive/Photos for storage providers
|
||||||
|
templ StorageProviderGDriveHeadlessAuth(ctx context.Context, data StorageProviderGDriveHeadlessAuthData) {
|
||||||
|
// Force the layout to display as authenticated content
|
||||||
|
@LayoutWithContext("Google Authentication - Headless Mode", ctx) {
|
||||||
|
<style>
|
||||||
|
/* Ensure proper styling for the headless auth page */
|
||||||
|
body.dark .auth-page {
|
||||||
|
background-color: #111827 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="auth-container" class="auth-page w-full pb-8 bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; background-color: rgb(249, 250, 251);">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-key w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Headless Google Authentication
|
||||||
|
</h1>
|
||||||
|
<a href="/storage-providers" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||||
|
Back to Storage Providers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 mb-6">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0 mt-0.5">
|
||||||
|
<i class="fas fa-info-circle h-5 w-5 text-blue-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
You need to authenticate with Google using a web browser. Since you're running GoMFT behind a reverse proxy or in a headless environment, you'll need to complete authentication on a machine with a web browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 1: Run the following command on a machine with a web browser</h2>
|
||||||
|
<div class="relative mb-4">
|
||||||
|
<pre id="auth-command-text" class="bg-gray-50 dark:bg-gray-900 rounded-md p-4 overflow-x-auto text-sm font-mono">{ data.AuthCommand }</pre>
|
||||||
|
<button id="copy-command" class="absolute top-2 right-2 bg-gray-200 dark:bg-gray-700 p-1.5 rounded hover:bg-gray-300 dark:hover:bg-gray-600" title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy h-5 w-5 text-gray-700 dark:text-gray-300"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-md font-medium mb-2 text-gray-900 dark:text-white">What this command does:</h3>
|
||||||
|
<ul class="list-disc ml-6 text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||||
|
<li>Opens a browser window on the machine where you run it</li>
|
||||||
|
<li>Allows you to authenticate with Google</li>
|
||||||
|
<li>Generates an authentication token</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 2: Paste the authentication token below</h2>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
After completing authentication in the browser, you'll receive a token. Copy and paste that token here:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="/storage-providers/gdrive-headless-token" method="POST" class="space-y-4">
|
||||||
|
<input type="hidden" name="provider_id" value={ data.ProviderID } />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="auth_token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Authentication Token</label>
|
||||||
|
<textarea
|
||||||
|
id="auth_token"
|
||||||
|
name="auth_token"
|
||||||
|
rows="5"
|
||||||
|
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
|
||||||
|
placeholder="Paste your authentication token here..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">The token will look like a long JSON string containing access credentials.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-6">
|
||||||
|
<a href="/storage-providers" class="mr-4 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-400">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Submit Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">This authentication process is necessary for GoMFT to access your Google Drive or Google Photos account.</p>
|
||||||
|
<p class="mt-1 text-gray-600 dark:text-gray-400">The token is only used for authentication and is stored securely. You'll only need to complete this process once for each storage provider.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set dark background color if in dark mode
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
|
document.getElementById('auth-container').style.backgroundColor = '#111827';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for theme changes
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
document.getElementById('auth-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the actual command text
|
||||||
|
const actualCommand = document.getElementById('auth-command-text').textContent.trim();
|
||||||
|
|
||||||
|
// Add click handler to copy button
|
||||||
|
document.getElementById('copy-command').addEventListener('click', function() {
|
||||||
|
// Copy the actual command text, not the template variable
|
||||||
|
navigator.clipboard.writeText(actualCommand).then(function() {
|
||||||
|
// Show a success message
|
||||||
|
const button = document.getElementById('copy-command');
|
||||||
|
const originalTitle = button.getAttribute('title');
|
||||||
|
button.setAttribute('title', 'Copied!');
|
||||||
|
|
||||||
|
// Also show visual feedback
|
||||||
|
button.classList.add('bg-green-200', 'dark:bg-green-700');
|
||||||
|
button.classList.remove('bg-gray-200', 'dark:bg-gray-700');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
button.setAttribute('title', originalTitle);
|
||||||
|
button.classList.remove('bg-green-200', 'dark:bg-green-700');
|
||||||
|
button.classList.add('bg-gray-200', 'dark:bg-gray-700');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to copy text: ', err);
|
||||||
|
alert('Failed to copy command. Please select and copy it manually.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,728 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/starfleetcptn/gomft/components/shared/toast"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageProvidersData struct {
|
||||||
|
Providers []db.StorageProvider
|
||||||
|
Error string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main template for Storage Providers page
|
||||||
|
templ StorageProviders(ctx context.Context, data StorageProvidersData) {
|
||||||
|
@LayoutWithContext("Storage Providers", ctx) {
|
||||||
|
@toast.Container()
|
||||||
|
@toast.ShowToastJS()
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Handle test provider button clicks
|
||||||
|
window.testProvider = function(button) {
|
||||||
|
const providerId = button.getAttribute('data-provider-id');
|
||||||
|
const providerName = button.getAttribute('data-provider-name') || `Provider #${providerId}`;
|
||||||
|
showToast(`Testing connection to "${providerName}"...`, 'info');
|
||||||
|
button.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
if (event.detail.successful) {
|
||||||
|
showToast(`Connection to "${providerName}" successful!`, 'success');
|
||||||
|
} else {
|
||||||
|
let errorMsg = `Failed to connect to "${providerName}"`;
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = error.error ? `Connection error: ${error.error}` : `Connection error: ${event.detail.xhr.responseText}`;
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg = `Connection error: ${event.detail.xhr.responseText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Duplicate Provider HTMX Event Handling (configs.templ style) ---
|
||||||
|
// Track all HTMX events for duplicate provider
|
||||||
|
document.addEventListener('htmx:beforeRequest', function(event) {
|
||||||
|
const path = event.detail.path;
|
||||||
|
const method = event.detail.verb;
|
||||||
|
if (path && method === 'POST' && path.match(/^\/storage-providers\/\d+\/duplicate$/)) {
|
||||||
|
window.isProviderDuplicateRequest = true;
|
||||||
|
// Store the provider name for toast
|
||||||
|
const providerId = path.match(/^\/storage-providers\/(\d+)\/duplicate$/)[1];
|
||||||
|
const btn = document.querySelector(`button[hx-post="/storage-providers/${providerId}/duplicate"]`);
|
||||||
|
if (btn) {
|
||||||
|
window.duplicatingProviderName = btn.getAttribute('data-provider-name') || `Provider #${providerId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
// Handle delete provider events (existing logic)
|
||||||
|
if (event.detail.pathInfo && event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/storage-providers\/\d+$/) &&
|
||||||
|
event.detail.verb === 'DELETE') {
|
||||||
|
const providerName = event.detail.elt.getAttribute('data-provider-name') || 'Provider';
|
||||||
|
if (event.detail.successful) {
|
||||||
|
showToast(`Provider "${providerName}" deleted successfully`, 'success');
|
||||||
|
} else {
|
||||||
|
let errorMsg = `Failed to delete provider "${providerName}"`;
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = error.error ? error.error : `Error: ${event.detail.xhr.responseText}`;
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle duplicate provider success
|
||||||
|
const isDuplicateRequest = window.isProviderDuplicateRequest &&
|
||||||
|
event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/storage-providers\/\d+\/duplicate$/);
|
||||||
|
if (isDuplicateRequest && event.detail.successful) {
|
||||||
|
const providerName = window.duplicatingProviderName || 'provider';
|
||||||
|
showToast(`Provider "${providerName}" duplicated successfully`, 'success');
|
||||||
|
window.isProviderDuplicateRequest = false;
|
||||||
|
window.duplicatingProviderName = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:responseError', function(event) {
|
||||||
|
// Handle any HTMX response error
|
||||||
|
let errorMsg = "An error occurred";
|
||||||
|
let entityName = "Operation";
|
||||||
|
|
||||||
|
// Get more context about the operation that failed
|
||||||
|
if (event.detail.elt && event.detail.elt.getAttribute('data-provider-name')) {
|
||||||
|
entityName = event.detail.elt.getAttribute('data-provider-name');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle duplicate provider errors
|
||||||
|
const isDuplicateRequest = window.isProviderDuplicateRequest &&
|
||||||
|
event.detail.pathInfo &&
|
||||||
|
event.detail.pathInfo.requestPath &&
|
||||||
|
event.detail.pathInfo.requestPath.match(/^\/storage-providers\/\d+\/duplicate$/);
|
||||||
|
if (isDuplicateRequest) {
|
||||||
|
const providerName = window.duplicatingProviderName || 'provider';
|
||||||
|
errorMsg = `Failed to duplicate provider "${providerName}"`;
|
||||||
|
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = error.error ? error.error : errorMsg;
|
||||||
|
} catch (e) {
|
||||||
|
if (event.detail.xhr.responseText.trim()) {
|
||||||
|
errorMsg = event.detail.xhr.responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
window.isProviderDuplicateRequest = false;
|
||||||
|
window.duplicatingProviderName = null;
|
||||||
|
}
|
||||||
|
// Handle other HTMX errors
|
||||||
|
else if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(event.detail.xhr.responseText);
|
||||||
|
errorMsg = error.error ? error.error : `Error during ${entityName} operation`;
|
||||||
|
} catch (e) {
|
||||||
|
if (event.detail.xhr.responseText.trim()) {
|
||||||
|
errorMsg = event.detail.xhr.responseText;
|
||||||
|
} else {
|
||||||
|
errorMsg = `Error during ${entityName} operation`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the visual error alert for all error types
|
||||||
|
const errorAlert = document.getElementById('htmx-error-alert');
|
||||||
|
const errorMessageSpan = document.getElementById('htmx-error-message');
|
||||||
|
if (errorAlert && errorMessageSpan) {
|
||||||
|
errorMessageSpan.textContent = errorMsg;
|
||||||
|
errorAlert.classList.remove('hidden');
|
||||||
|
// Auto-hide after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
errorAlert.classList.add('hidden');
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a flag before duplicate request to show toast after reload
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const btn = e.target.closest('button[data-provider-id][hx-post*="/duplicate"]');
|
||||||
|
if (btn) {
|
||||||
|
localStorage.setItem('showProviderDuplicateToast', '1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Display error from data.Error if present
|
||||||
|
const errorElement = document.getElementById('provider-error-message');
|
||||||
|
if (errorElement && errorElement.textContent.trim()) {
|
||||||
|
showToast(errorElement.textContent.trim(), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display status messages programmatically
|
||||||
|
const statusElement = document.getElementById('provider-status-message');
|
||||||
|
if (statusElement && statusElement.textContent.trim()) {
|
||||||
|
const status = statusElement.textContent.trim();
|
||||||
|
let message = '';
|
||||||
|
let type = 'info';
|
||||||
|
|
||||||
|
// Convert status to appropriate toast message
|
||||||
|
switch(status) {
|
||||||
|
case 'created':
|
||||||
|
message = 'Provider created successfully';
|
||||||
|
type = 'success';
|
||||||
|
break;
|
||||||
|
case 'updated':
|
||||||
|
message = 'Provider updated successfully';
|
||||||
|
type = 'success';
|
||||||
|
break;
|
||||||
|
case 'deleted':
|
||||||
|
message = 'Provider deleted successfully';
|
||||||
|
type = 'success';
|
||||||
|
break;
|
||||||
|
case 'duplicated':
|
||||||
|
message = 'Provider duplicated successfully';
|
||||||
|
type = 'success';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (status) {
|
||||||
|
message = `Provider ${status}`;
|
||||||
|
type = 'info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
showToast(message, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toasts based on localStorage or URL parameters
|
||||||
|
if (localStorage.getItem('showProviderDuplicateToast')) {
|
||||||
|
showToast('Provider duplicated successfully', 'success');
|
||||||
|
localStorage.removeItem('showProviderDuplicateToast');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process URL parameters for toast notifications
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Handle combined status and test_status parameters
|
||||||
|
if (urlParams.get('status') === 'created') {
|
||||||
|
const testStatus = urlParams.get('test_status');
|
||||||
|
const errorMsg = urlParams.get('error');
|
||||||
|
|
||||||
|
if (testStatus === 'success') {
|
||||||
|
showToast('Provider created and tested successfully', 'success');
|
||||||
|
} else if (testStatus === 'failed' && errorMsg) {
|
||||||
|
showToast(`Provider created but test failed: ${errorMsg}`, 'error');
|
||||||
|
} else {
|
||||||
|
showToast('Provider created successfully', 'success');
|
||||||
|
}
|
||||||
|
} else if (urlParams.get('status') === 'updated') {
|
||||||
|
const testStatus = urlParams.get('test_status');
|
||||||
|
const errorMsg = urlParams.get('error');
|
||||||
|
|
||||||
|
if (testStatus === 'success') {
|
||||||
|
showToast('Provider updated and tested successfully', 'success');
|
||||||
|
} else if (testStatus === 'failed' && errorMsg) {
|
||||||
|
showToast(`Provider updated but test failed: ${errorMsg}`, 'error');
|
||||||
|
} else {
|
||||||
|
showToast('Provider updated successfully', 'success');
|
||||||
|
}
|
||||||
|
} else if (urlParams.get('error')) {
|
||||||
|
showToast(urlParams.get('error'), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Error message holder (hidden, used to pass server-side errors to JS) -->
|
||||||
|
if data.Error != "" {
|
||||||
|
<div id="provider-error-message" class="hidden">{ data.Error }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Status message holder (hidden, used to pass server-side status to JS) -->
|
||||||
|
if data.Status != "" {
|
||||||
|
<div id="provider-status-message" class="hidden">{ data.Status }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="providers-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="providers-page bg-gray-50 dark:bg-gray-900">
|
||||||
|
<!-- Providers list follows; import button is above -->
|
||||||
|
<div class="pb-8 w-full">
|
||||||
|
<!-- Display error alert if data.Error is not empty -->
|
||||||
|
if data.Error != "" {
|
||||||
|
<div class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle flex-shrink-0 mr-2"></i>
|
||||||
|
<span>{ data.Error }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Dynamic error alert container for HTMX errors -->
|
||||||
|
// <div id="htmx-error-alert" class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400 hidden" role="alert">
|
||||||
|
// <div class="flex items-center">
|
||||||
|
// <i class="fas fa-exclamation-circle flex-shrink-0 mr-2"></i>
|
||||||
|
// <span id="htmx-error-message"></span>
|
||||||
|
// <button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-red-50 text-red-500 rounded-lg focus:ring-2 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700" onclick="document.getElementById('htmx-error-alert').classList.add('hidden')">
|
||||||
|
// <span class="sr-only">Dismiss</span>
|
||||||
|
// <i class="fas fa-times"></i>
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-server w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Storage Providers
|
||||||
|
</h1>
|
||||||
|
<!-- Import rclone config and New Provider links side by side -->
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
<a href="/storage-providers/import" class="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded-lg shadow flex items-center">
|
||||||
|
<i class="fas fa-file-import mr-2"></i>
|
||||||
|
Import rclone config
|
||||||
|
</a>
|
||||||
|
<a href="/storage-providers/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
New Provider
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
if len(data.Providers) == 0 {
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-8 flex flex-col items-center justify-center text-center">
|
||||||
|
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||||
|
<i class="fas fa-server text-gray-400 dark:text-gray-500 text-3xl"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No storage providers</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating a new storage provider.</p>
|
||||||
|
<a href="/storage-providers/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||||
|
Create First Provider
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
@StorageProviders_ProvidersList(data.Providers)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Section -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Storage providers define connection details to different storage systems.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-shield-alt w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Your credentials are encrypted for security. You can test connections before using them in transfers.</p>
|
||||||
|
<p class="mt-1 text-gray-600 dark:text-gray-400">Google Drive and Google Photos providers require authentication. Click the "Authenticate" button to complete setup.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start mt-4">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<i class="fas fa-link w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 text-sm">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">Providers can be used in multiple transfer configurations. Deleting a provider will affect any transfer that uses it.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/storage-providers.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Function to initialize all auth dropdowns
|
||||||
|
function initAllAuthDropdowns() {
|
||||||
|
// Get all dropdown buttons
|
||||||
|
const dropdownButtons = document.querySelectorAll('[id^="auth-dropdown-button-"]');
|
||||||
|
|
||||||
|
dropdownButtons.forEach(button => {
|
||||||
|
const providerId = button.getAttribute('data-provider-id');
|
||||||
|
const menu = document.getElementById(`auth-dropdown-menu-${providerId}`);
|
||||||
|
|
||||||
|
if (button && menu) {
|
||||||
|
// Add click listener
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Position dropdown based on available space
|
||||||
|
const buttonRect = button.getBoundingClientRect();
|
||||||
|
const spaceRight = window.innerWidth - buttonRect.right;
|
||||||
|
const spaceLeft = buttonRect.left;
|
||||||
|
|
||||||
|
// Check if there's more space on the left or right side
|
||||||
|
if (spaceLeft > spaceRight) {
|
||||||
|
menu.classList.add('right-0');
|
||||||
|
menu.classList.remove('left-0');
|
||||||
|
} else {
|
||||||
|
menu.classList.add('left-0');
|
||||||
|
menu.classList.remove('right-0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle visibility
|
||||||
|
menu.classList.toggle('hidden');
|
||||||
|
console.log(`Toggled dropdown for provider ${providerId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Initialized dropdown for provider ${providerId}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Could not find dropdown elements for provider ${providerId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdowns when clicking elsewhere
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
dropdownButtons.forEach(button => {
|
||||||
|
const providerId = button.getAttribute('data-provider-id');
|
||||||
|
const menu = document.getElementById(`auth-dropdown-menu-${providerId}`);
|
||||||
|
|
||||||
|
if (menu && !button.contains(e.target) && !menu.contains(e.target)) {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize dropdowns when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Check for status messages based on URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.get('status') === 'gdrive_auth_success') {
|
||||||
|
showToast("Google Drive authentication completed successfully", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all authentication dropdowns
|
||||||
|
initAllAuthDropdowns();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial for just the providers-list div
|
||||||
|
// Usage: @StorageProviders_ProvidersList(providers)
|
||||||
|
templ StorageProviders_ProvidersList(providers []db.StorageProvider) {
|
||||||
|
<div id="providers-list" class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||||
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
for _, provider := range providers {
|
||||||
|
<li>
|
||||||
|
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<div class="px-4 py-4 sm:px-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||||
|
{ provider.Name }
|
||||||
|
</p>
|
||||||
|
<span class="ml-2 bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
{ string(provider.Type) }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||||
|
<!-- Test Connection Button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
hx-post={ fmt.Sprintf("/storage-providers/%d/test", provider.ID) }
|
||||||
|
hx-swap="none"
|
||||||
|
data-provider-id={ fmt.Sprint(provider.ID) }
|
||||||
|
data-provider-name={ provider.Name }
|
||||||
|
onclick="window.testProvider(this)"
|
||||||
|
class="test-provider-btn text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-blue-700 dark:text-blue-300 dark:hover:bg-blue-600 dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-plug w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
<!-- Duplicate Button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
hx-post={ fmt.Sprintf("/storage-providers/%d/duplicate", provider.ID) }
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="#providers-list"
|
||||||
|
data-provider-id={ fmt.Sprint(provider.ID) }
|
||||||
|
data-provider-name={ provider.Name }
|
||||||
|
class="text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:ring-4 focus:outline-none focus:ring-indigo-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-indigo-700 dark:text-indigo-300 dark:hover:bg-indigo-600 dark:focus:ring-indigo-800">
|
||||||
|
<i class="fas fa-clone w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d", provider.ID)) } class="text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:focus:ring-gray-700">
|
||||||
|
<i class="fas fa-edit w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<!-- Delete Button -->
|
||||||
|
@StorageProviderDialog(
|
||||||
|
fmt.Sprintf("delete-provider-dialog-%d", provider.ID),
|
||||||
|
"Delete Provider",
|
||||||
|
fmt.Sprintf("Are you sure you want to delete the provider '%s'? This cannot be undone.", provider.Name),
|
||||||
|
"text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center",
|
||||||
|
"Delete",
|
||||||
|
"delete",
|
||||||
|
provider.ID,
|
||||||
|
provider.Name,
|
||||||
|
)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={ showProviderModal(fmt.Sprintf("delete-provider-dialog-%d", provider.ID)) }
|
||||||
|
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
|
||||||
|
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 sm:flex sm:justify-between">
|
||||||
|
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||||
|
<!-- Show different details based on provider type -->
|
||||||
|
if provider.Type == "local" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-folder w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Path: { provider.Host }
|
||||||
|
</p>
|
||||||
|
} else if provider.Type == "s3" || provider.Type == "wasabi" || provider.Type == "minio" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-server w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Endpoint: { provider.Host }
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-box w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Bucket: { provider.Bucket }
|
||||||
|
</p>
|
||||||
|
if provider.Region != "" {
|
||||||
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-globe w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Region: { provider.Region }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
} else if provider.Type == "drive" || provider.Type == "gphotos" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fab fa-google-drive w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Google
|
||||||
|
if provider.Type == "drive" {
|
||||||
|
Drive
|
||||||
|
} else {
|
||||||
|
Photos
|
||||||
|
}
|
||||||
|
if provider.DriveID != "" {
|
||||||
|
: { provider.DriveID }
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
if provider.Authenticated != nil && *provider.Authenticated {
|
||||||
|
<span class="mt-2 md:mt-0 bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||||
|
<i class="fas fa-check-circle w-3 h-3 mr-1 inline"></i>
|
||||||
|
Authenticated
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<div class="flex flex-col md:flex-row items-start md:items-center mt-2 md:mt-0">
|
||||||
|
|
||||||
|
<!-- Google Authentication Dropdown -->
|
||||||
|
<div class="relative inline-block text-left mt-2 md:mt-0">
|
||||||
|
<button
|
||||||
|
id={ fmt.Sprintf("auth-dropdown-button-%d", provider.ID) }
|
||||||
|
data-provider-id={ fmt.Sprintf("%d", provider.ID) }
|
||||||
|
type="button"
|
||||||
|
class="text-yellow-700 bg-yellow-100 hover:bg-yellow-200 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-yellow-900 dark:text-yellow-300 dark:hover:bg-yellow-800 dark:focus:ring-yellow-800"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true">
|
||||||
|
<i class="fas fa-key w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Authenticate with Google
|
||||||
|
<i class="fas fa-chevron-down w-3.5 h-3.5 ml-1.5"></i>
|
||||||
|
</button>
|
||||||
|
<div id={ fmt.Sprintf("auth-dropdown-menu-%d", provider.ID) } class="origin-top-right absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-700 ring-1 ring-black ring-opacity-5 focus:outline-none z-50 hidden" style="max-height: 200px; overflow-y: auto;" role="menu" aria-orientation="vertical" aria-labelledby={ fmt.Sprintf("auth-dropdown-button-%d", provider.ID) }>
|
||||||
|
<div class="py-1" role="none">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 group flex items-center px-4 py-2 text-sm" role="menuitem">
|
||||||
|
<i class="fas fa-globe w-4 h-4 mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Standard Authentication
|
||||||
|
</a>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 group flex items-center px-4 py-2 text-sm" role="menuitem">
|
||||||
|
<i class="fas fa-terminal w-4 h-4 mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||||
|
Headless Authentication
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden fallback links - only shown when JavaScript is disabled -->
|
||||||
|
<noscript>
|
||||||
|
<div class="flex flex-col text-xs text-gray-500 dark:text-gray-400 mt-1 ml-1">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="hover:underline hover:text-blue-500">
|
||||||
|
Direct Standard Auth
|
||||||
|
</a>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="hover:underline hover:text-blue-500">
|
||||||
|
Direct Headless Auth
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else if provider.Type == "webdav" || provider.Type == "nextcloud" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-cloud w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Server: { provider.Host }
|
||||||
|
</p>
|
||||||
|
if provider.Username != "" {
|
||||||
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-user w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
User: { provider.Username }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
} else if provider.Type == "sftp" || provider.Type == "hetzner" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-server w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Host: { provider.Host }
|
||||||
|
if provider.Port > 0 {
|
||||||
|
:{ fmt.Sprint(provider.Port) }
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
if provider.Username != "" {
|
||||||
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-user w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
User: { provider.Username }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
} else if provider.Type == "b2" {
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-cloud w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Backblaze B2
|
||||||
|
</p>
|
||||||
|
if provider.Bucket != "" {
|
||||||
|
<p class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-box w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Bucket: { provider.Bucket }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<!-- Default display for other provider types -->
|
||||||
|
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-server w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
Host: { provider.Host }
|
||||||
|
if provider.Port > 0 {
|
||||||
|
:{ fmt.Sprint(provider.Port) }
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||||
|
<p>
|
||||||
|
Updated: { provider.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Authentication Notice (More Visible) -->
|
||||||
|
if (provider.Type == "drive" || provider.Type == "gphotos") && (provider.Authenticated == nil || !*provider.Authenticated) {
|
||||||
|
<div class="mt-3 flex items-center justify-between bg-yellow-50 dark:bg-yellow-900/30 rounded-lg p-3 border border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-500 w-5 h-5 mr-2"></i>
|
||||||
|
<span class="text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
|
Authentication required for Google
|
||||||
|
if provider.Type == "drive" {
|
||||||
|
Drive
|
||||||
|
} else {
|
||||||
|
Photos
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded-lg">
|
||||||
|
<i class="fas fa-globe w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Standard Auth
|
||||||
|
</a>
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="text-sm font-medium text-white bg-green-600 hover:bg-green-700 px-3 py-1.5 rounded-lg">
|
||||||
|
<i class="fas fa-terminal w-3.5 h-3.5 mr-1.5"></i>
|
||||||
|
Headless Auth
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog component for confirmation dialogs for storage providers
|
||||||
|
// Usage: @StorageProviderDialog(id, title, message, confirmClass, confirmText, action, providerID, providerName)
|
||||||
|
templ StorageProviderDialog(id string, title string, message string, confirmClass string, confirmText string, action string, providerID uint, providerName string) {
|
||||||
|
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ confirmClass }
|
||||||
|
hx-delete={ fmt.Sprintf("/storage-providers/%d", providerID) }
|
||||||
|
hx-target="closest li"
|
||||||
|
hx-swap="delete"
|
||||||
|
data-provider-name={ providerName }
|
||||||
|
data-provider-id={ fmt.Sprint(providerID) }
|
||||||
|
id={ fmt.Sprintf("delete-provider-btn-%d", providerID) }
|
||||||
|
onclick={ triggerProviderDelete(id, providerID, providerName) }>
|
||||||
|
{ confirmText }
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick={ closeProviderModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
script closeProviderModal(id string) {
|
||||||
|
const modal = document.getElementById(id);
|
||||||
|
const backdrop = document.getElementById(id + '-backdrop');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.remove();
|
||||||
|
}
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
script showProviderModal(id string) {
|
||||||
|
const modal = document.getElementById(id);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
script triggerProviderDelete(dialogId string, providerID uint, providerName string) {
|
||||||
|
// Hide the dialog
|
||||||
|
document.getElementById(dialogId).classList.add("hidden");
|
||||||
|
document.getElementById(dialogId).classList.remove("flex");
|
||||||
|
// Store data for event handlers
|
||||||
|
window.lastDeletedProvider = {
|
||||||
|
id: providerID,
|
||||||
|
name: providerName
|
||||||
|
};
|
||||||
|
window.currentlyDeletingProvider = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage Providers Import Page
|
||||||
|
// User uploads a config, previews/edit remotes, selects which to import
|
||||||
|
// On submit, POSTs selected remotes to /storage-providers/import/confirm
|
||||||
|
|
||||||
|
// Main import page template
|
||||||
|
templ StorageProvidersImport(ctx context.Context, preview RcloneImportPreview) {
|
||||||
|
<div id="providers-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="providers-page bg-gray-50 dark:bg-gray-900 pb-8 w-full">
|
||||||
|
<!-- Header with back button -->
|
||||||
|
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-file-import w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||||
|
Import rclone Config
|
||||||
|
</h1>
|
||||||
|
<a href="/storage-providers" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-white focus:outline-none dark:focus:ring-gray-800">
|
||||||
|
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||||
|
Back to Providers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-3">Import from rclone Config</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||||
|
Upload your rclone configuration file to import storage providers. You'll be able to preview and select which remotes to import, and add any missing credentials.
|
||||||
|
</p>
|
||||||
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400" role="alert">
|
||||||
|
<div class="flex">
|
||||||
|
<i class="fas fa-info-circle flex-shrink-0 inline w-5 h-5 mr-3 mt-0.5"></i>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Instructions:</span>
|
||||||
|
<ul class="mt-1.5 ml-4 list-disc">
|
||||||
|
<li>Upload your <code>rclone.conf</code> file (usually found in <code>~/.config/rclone/</code> or <code>%USERPROFILE%\.config\rclone\</code>)</li>
|
||||||
|
<li>Review the detected remotes and select which ones to import</li>
|
||||||
|
<li>Add any missing credentials that may not be in your config file</li>
|
||||||
|
<li>Use the "Add Custom Field" button to add any provider-specific options</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="https://rclone.org/docs/" target="_blank" class="text-blue-600 dark:text-blue-500 underline hover:no-underline">rclone documentation</a> |
|
||||||
|
<button type="button" id="show-common-values" class="text-blue-600 dark:text-blue-500 underline hover:no-underline">Show common configuration values</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Common values reference (hidden by default) -->
|
||||||
|
<div id="common-values-reference" class="hidden p-4 mb-4 text-sm text-gray-800 rounded-lg bg-gray-50 dark:bg-gray-800 dark:text-gray-300 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="font-medium text-base mb-2">Common Configuration Values by Provider Type</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">SFTP</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>host</code>: Server hostname or IP</li>
|
||||||
|
<li><code>user</code>: Username</li>
|
||||||
|
<li><code>pass</code>: Password (if not using key)</li>
|
||||||
|
<li><code>port</code>: SSH port (default: 22)</li>
|
||||||
|
<li><code>key_file</code>: Path to private key</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">S3 / Wasabi / Minio</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>access_key_id</code>: Access key</li>
|
||||||
|
<li><code>secret_access_key</code>: Secret key</li>
|
||||||
|
<li><code>region</code>: Region name</li>
|
||||||
|
<li><code>endpoint</code>: Custom endpoint URL</li>
|
||||||
|
<li><code>bucket</code>: Bucket name</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">FTP</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>host</code>: Server hostname or IP</li>
|
||||||
|
<li><code>user</code>: Username</li>
|
||||||
|
<li><code>pass</code>: Password</li>
|
||||||
|
<li><code>port</code>: FTP port (default: 21)</li>
|
||||||
|
<li><code>tls</code>: Use FTPS (true/false)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">Google Drive</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>client_id</code>: OAuth client ID</li>
|
||||||
|
<li><code>client_secret</code>: OAuth client secret</li>
|
||||||
|
<li><code>refresh_token</code>: OAuth refresh token</li>
|
||||||
|
<li><code>team_drive</code>: Team Drive ID (optional)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">OneDrive</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>client_id</code>: OAuth client ID</li>
|
||||||
|
<li><code>client_secret</code>: OAuth client secret</li>
|
||||||
|
<li><code>refresh_token</code>: OAuth refresh token</li>
|
||||||
|
<li><code>drive_id</code>: Drive ID (optional)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">WebDAV / Nextcloud</h4>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li><code>url</code>: WebDAV URL</li>
|
||||||
|
<li><code>user</code>: Username</li>
|
||||||
|
<li><code>pass</code>: Password</li>
|
||||||
|
<li><code>vendor</code>: nextcloud/owncloud/etc</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="hide-common-values" class="mt-3 text-blue-600 dark:text-blue-500 underline hover:no-underline">Hide reference</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="rclone-upload-form" enctype="multipart/form-data" method="POST" action="/storage-providers/import/preview" hx-post="/storage-providers/import/preview" hx-target="#import-preview" hx-swap="innerHTML" class="mt-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Select rclone config file:</label>
|
||||||
|
<input type="file" name="rclone_config" accept=".conf,.txt,.ini,.cfg" required class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400" />
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Accepted formats: .conf, .txt, .ini, .cfg</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
|
<i class="fas fa-search mr-2"></i>
|
||||||
|
Preview Remotes
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="import-preview" class="mt-6">
|
||||||
|
@RcloneImportPreviewContent(ctx, preview)
|
||||||
|
</div>
|
||||||
|
<div id="import-result" class="mt-6"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just the preview content for HTMX updates
|
||||||
|
templ RcloneImportPreviewContent(ctx context.Context, preview RcloneImportPreview) {
|
||||||
|
if preview.Error != "" {
|
||||||
|
<div class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-circle flex-shrink-0 mr-2"></i>
|
||||||
|
<span>{preview.Error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else if preview.Remotes != nil && len(preview.Remotes) > 0 {
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Found {fmt.Sprintf("%d", len(preview.Remotes))} remotes</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 mb-4">Select which remotes to import and edit their details if needed.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="confirm-import-form" method="POST" action="/storage-providers/import/confirm" hx-post="/storage-providers/import/confirm" hx-target="#import-result" hx-swap="innerHTML">
|
||||||
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" checked class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" id="select-all-checkbox" onclick="toggleAllCheckboxes(this)" />
|
||||||
|
<label for="select-all-checkbox" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">Import?</label>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Name</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Type</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Fields</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
if len(preview.Remotes) > 0 {
|
||||||
|
for _, remote := range preview.Remotes {
|
||||||
|
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox" name={"import_" + remote.Name} checked class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="text" name={"name_" + remote.Name} value={remote.Name} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="text" name={"type_" + remote.Name} value={remote.Type} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<!-- Existing fields from rclone config -->
|
||||||
|
for k, v := range remote.Fields {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">{k}:</span>
|
||||||
|
<input type="text" name={"field_" + remote.Name + "_" + k} value={v} class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Add missing credentials section -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-white mb-2">Add or Update Credentials</div>
|
||||||
|
|
||||||
|
<!-- Username field (if not present) -->
|
||||||
|
if _, exists := remote.Fields["user"]; !exists && (remote.Type == "sftp" || remote.Type == "ftp" || remote.Type == "smb" || remote.Type == "webdav") {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">user:</span>
|
||||||
|
<input type="text" name={"field_" + remote.Name + "_user"} placeholder="Username" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Password field (if not present) -->
|
||||||
|
if _, exists := remote.Fields["pass"]; !exists && (remote.Type == "sftp" || remote.Type == "ftp" || remote.Type == "smb" || remote.Type == "webdav") {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">pass:</span>
|
||||||
|
<input type="password" name={"field_" + remote.Name + "_pass"} placeholder="Password" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- S3 credentials -->
|
||||||
|
if remote.Type == "s3" || remote.Type == "wasabi" || remote.Type == "minio" || remote.Type == "b2" {
|
||||||
|
<!-- Access Key -->
|
||||||
|
if _, exists := remote.Fields["access_key_id"]; !exists {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">access_key_id:</span>
|
||||||
|
<input type="text" name={"field_" + remote.Name + "_access_key_id"} placeholder="Access Key" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Secret Key -->
|
||||||
|
if _, exists := remote.Fields["secret_access_key"]; !exists {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">secret_access_key:</span>
|
||||||
|
<input type="password" name={"field_" + remote.Name + "_secret_access_key"} placeholder="Secret Key" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- OAuth credentials -->
|
||||||
|
if remote.Type == "drive" || remote.Type == "onedrive" || remote.Type == "gphotos" {
|
||||||
|
<!-- Client ID -->
|
||||||
|
if _, exists := remote.Fields["client_id"]; !exists {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">client_id:</span>
|
||||||
|
<input type="text" name={"field_" + remote.Name + "_client_id"} placeholder="Client ID" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Client Secret -->
|
||||||
|
if _, exists := remote.Fields["client_secret"]; !exists {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300 min-w-[80px]">client_secret:</span>
|
||||||
|
<input type="password" name={"field_" + remote.Name + "_client_secret"} placeholder="Client Secret" class="bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Add custom field button -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" id={"add-field-btn-" + remote.Name} class="text-xs text-blue-700 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-plus mr-1"></i> Add Custom Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container for dynamically added custom fields -->
|
||||||
|
<div id={"custom-fields-" + remote.Name} class="mt-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">No remotes found in config.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800">
|
||||||
|
<i class="fas fa-file-import mr-2"></i>
|
||||||
|
Import Selected Remotes
|
||||||
|
</button>
|
||||||
|
<a href="/storage-providers" class="ml-2 text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleAllCheckboxes(source) {
|
||||||
|
const checkboxes = document.querySelectorAll('input[type="checkbox"][name^="import_"]');
|
||||||
|
for (let i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = source.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomField(remoteName) {
|
||||||
|
console.log('Adding custom field for remote:', remoteName);
|
||||||
|
const container = document.getElementById('custom-fields-' + remoteName);
|
||||||
|
if (!container) {
|
||||||
|
console.error('Container not found for remote:', remoteName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldCount = container.children.length;
|
||||||
|
const fieldId = 'custom-field-' + remoteName + '-' + fieldCount;
|
||||||
|
|
||||||
|
const fieldRow = document.createElement('div');
|
||||||
|
fieldRow.className = 'flex items-center gap-2 mt-2';
|
||||||
|
fieldRow.id = fieldId;
|
||||||
|
|
||||||
|
// Create key input
|
||||||
|
const keyInput = document.createElement('input');
|
||||||
|
keyInput.type = 'text';
|
||||||
|
keyInput.className = 'bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 w-1/3 p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500';
|
||||||
|
keyInput.placeholder = 'Field name';
|
||||||
|
keyInput.id = 'custom-key-' + remoteName + '-' + fieldCount;
|
||||||
|
keyInput.onchange = function() {
|
||||||
|
const valueInput = document.getElementById('custom-value-' + remoteName + '-' + fieldCount);
|
||||||
|
if (valueInput && this.value) {
|
||||||
|
valueInput.name = 'field_' + remoteName + '_' + this.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create value input
|
||||||
|
const valueInput = document.createElement('input');
|
||||||
|
valueInput.type = 'text';
|
||||||
|
valueInput.className = 'bg-gray-50 border border-gray-300 text-gray-900 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 w-2/3 p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500';
|
||||||
|
valueInput.placeholder = 'Value';
|
||||||
|
valueInput.id = 'custom-value-' + remoteName + '-' + fieldCount;
|
||||||
|
// Name will be set when key changes
|
||||||
|
|
||||||
|
// Create remove button
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300';
|
||||||
|
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
removeBtn.onclick = function() {
|
||||||
|
document.getElementById(fieldId).remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add elements to the row
|
||||||
|
fieldRow.appendChild(keyInput);
|
||||||
|
fieldRow.appendChild(valueInput);
|
||||||
|
fieldRow.appendChild(removeBtn);
|
||||||
|
|
||||||
|
// Add row to container
|
||||||
|
container.appendChild(fieldRow);
|
||||||
|
|
||||||
|
// Focus on the new key input
|
||||||
|
keyInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the buttons directly instead of using DOMContentLoaded
|
||||||
|
function initializeCustomFieldButtons() {
|
||||||
|
console.log('Initializing custom field buttons');
|
||||||
|
const addFieldButtons = document.querySelectorAll('[id^="add-field-btn-"]');
|
||||||
|
console.log('Found buttons:', addFieldButtons.length);
|
||||||
|
|
||||||
|
// Add click event listeners to each button
|
||||||
|
addFieldButtons.forEach(function(button) {
|
||||||
|
const remoteName = button.id.replace('add-field-btn-', '');
|
||||||
|
console.log('Adding listener for remote:', remoteName);
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
console.log('Button clicked for remote:', remoteName);
|
||||||
|
addCustomField(remoteName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try both approaches for maximum compatibility
|
||||||
|
// 1. Initialize immediately if document is already loaded
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
setTimeout(initializeCustomFieldButtons, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Also listen for DOMContentLoaded
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeCustomFieldButtons);
|
||||||
|
|
||||||
|
// 3. Also initialize when the form is loaded via HTMX
|
||||||
|
document.addEventListener('htmx:afterSwap', function(event) {
|
||||||
|
if (event.detail.target.id === 'import-preview') {
|
||||||
|
console.log('HTMX content loaded, initializing buttons');
|
||||||
|
setTimeout(initializeCustomFieldButtons, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Toggle common values reference
|
||||||
|
function initializeReferenceToggle() {
|
||||||
|
console.log('Initializing reference toggle buttons');
|
||||||
|
const showBtn = document.getElementById('show-common-values');
|
||||||
|
const hideBtn = document.getElementById('hide-common-values');
|
||||||
|
const reference = document.getElementById('common-values-reference');
|
||||||
|
|
||||||
|
if (showBtn && hideBtn && reference) {
|
||||||
|
console.log('Found reference toggle elements');
|
||||||
|
// Remove any existing listeners to prevent duplicates
|
||||||
|
showBtn.removeEventListener('click', showReference);
|
||||||
|
hideBtn.removeEventListener('click', hideReference);
|
||||||
|
|
||||||
|
// Add new listeners
|
||||||
|
showBtn.addEventListener('click', showReference);
|
||||||
|
hideBtn.addEventListener('click', hideReference);
|
||||||
|
|
||||||
|
// Define the functions
|
||||||
|
function showReference() {
|
||||||
|
console.log('Showing reference');
|
||||||
|
reference.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideReference() {
|
||||||
|
console.log('Hiding reference');
|
||||||
|
reference.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Reference toggle elements not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on DOMContentLoaded
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeReferenceToggle);
|
||||||
|
|
||||||
|
// Also initialize on page load
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
setTimeout(initializeReferenceToggle, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also initialize when content is loaded via HTMX
|
||||||
|
document.addEventListener('htmx:afterSwap', function(event) {
|
||||||
|
console.log('HTMX content swapped, target:', event.detail.target.id);
|
||||||
|
setTimeout(initializeReferenceToggle, 1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<div class="flex p-4 mb-4 text-sm text-gray-800 border border-gray-300 rounded-lg bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600" role="alert">
|
||||||
|
<i class="fas fa-info-circle flex-shrink-0 inline w-5 h-5 mr-3"></i>
|
||||||
|
<span class="sr-only">Info</span>
|
||||||
|
<div>
|
||||||
|
Upload a config file to preview rclone remotes for import.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StorageProvidersImportPage wraps the import component with the layout
|
||||||
|
templ StorageProvidersImportPage(ctx context.Context, preview RcloneImportPreview) {
|
||||||
|
@LayoutWithContext("Import rclone Config", ctx) {
|
||||||
|
@StorageProvidersImport(ctx, preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Keep the screenshots directory README
|
||||||
|
!/static/screenshots/README.md
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# GoMFT Documentation
|
||||||
|
|
||||||
|
This repository contains the documentation for [GoMFT](https://github.com/StarFleetCPTN/GoMFT), a modern, web-based managed file transfer solution written in Go.
|
||||||
|
|
||||||
|
[](https://discord.gg/f9dwtM3j)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the static site
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The built files will be in the `build` directory.
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
- **Introduction**: Overview and features of GoMFT
|
||||||
|
- **Getting Started**: Installation guides and quick start
|
||||||
|
- **Core Concepts**: Detailed information about transfers, connections, and schedules
|
||||||
|
- **Advanced Features**: Webhooks, email notifications, and admin tools
|
||||||
|
- **Security**: Security best practices and configuration
|
||||||
|
- **Development**: Project structure and contributing guidelines
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions to the documentation are welcome! Please submit a PR with your changes.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
Join our [Discord community](https://discord.gg/f9dwtM3j) for support, discussions, and updates about GoMFT.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This documentation is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Admin Tools
|
||||||
|
---
|
||||||
|
|
||||||
|
# Admin Tools
|
||||||
|
|
||||||
|
GoMFT provides a comprehensive set of administrative tools for system management, monitoring, and maintenance. These tools help administrators maintain the system, troubleshoot issues, and ensure optimal performance.
|
||||||
|
|
||||||
|
## Log Viewer
|
||||||
|
|
||||||
|
The Admin Tools panel includes an integrated log viewer with the following features:
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Log File Browser**: View a list of all available log files in the system
|
||||||
|
- **Real-time Log Viewing**: View log file contents directly in the web interface
|
||||||
|
- **Refresh Function**: Update the log list and content with the latest information
|
||||||
|
- **User-friendly Interface**: Clean, readable presentation with custom scrolling
|
||||||
|
- **Dark Mode Support**: Consistent theming with the rest of the application
|
||||||
|
- **Navigation**: Easily switch between different log files
|
||||||
|
|
||||||
|
### Available Logs
|
||||||
|
|
||||||
|
- **Application Logs**: General application logs
|
||||||
|
- **Transfer Logs**: Detailed logs of file transfer operations
|
||||||
|
- **Authentication Logs**: Login attempts and authentication events
|
||||||
|
- **Scheduler Logs**: Information about scheduled job execution
|
||||||
|
- **API Logs**: API usage and requests
|
||||||
|
- **Webhook Logs**: Records of webhook delivery attempts
|
||||||
|
- **Email Logs**: Email sending attempts and errors
|
||||||
|
|
||||||
|
### Using the Log Viewer
|
||||||
|
|
||||||
|
1. Select a log category from the dropdown menu
|
||||||
|
2. Choose a specific log file from the list
|
||||||
|
3. View the log content in the main panel
|
||||||
|
4. Use the search function to find specific text
|
||||||
|
5. Click **Refresh** to update with the latest entries
|
||||||
|
|
||||||
|
## Database Management
|
||||||
|
|
||||||
|
The Admin Tools interface also includes database management capabilities:
|
||||||
|
|
||||||
|
### Backup Management
|
||||||
|
|
||||||
|
- **Create Backup**: Generate a backup of the GoMFT database
|
||||||
|
- **Schedule Backups**: Configure automatic backup schedules
|
||||||
|
- **View Backups**: List all available backups with their dates and sizes
|
||||||
|
- **Download Backup**: Download a backup file for safekeeping
|
||||||
|
- **Restore Backup**: Restore the system from a previous backup
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
|
||||||
|
- **Optimize Database**: Run maintenance tasks to optimize performance
|
||||||
|
- **Check Integrity**: Verify database integrity and identify issues
|
||||||
|
- **Vacuum Database**: Reclaim unused space in the database
|
||||||
|
- **View Statistics**: Get database size and table statistics
|
||||||
|
|
||||||
|
## System Information
|
||||||
|
|
||||||
|
The System Information panel provides a comprehensive overview of your GoMFT installation:
|
||||||
|
|
||||||
|
### System Stats
|
||||||
|
|
||||||
|
- **Version Information**: Current GoMFT version and build details
|
||||||
|
- **System Resources**: CPU, memory, and disk usage
|
||||||
|
- **Uptime**: System uptime and start time
|
||||||
|
- **Active Transfers**: Currently running transfers
|
||||||
|
- **Queued Transfers**: Transfers waiting to be executed
|
||||||
|
- **Database Size**: Current size of the database
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
- **Service Status**: Status of all system services
|
||||||
|
- **Storage Space**: Available space in data directories
|
||||||
|
- **Connection Tests**: Tests for external services like SMTP
|
||||||
|
- **Rclone Status**: Verify rclone availability and version
|
||||||
|
|
||||||
|
## User Management
|
||||||
|
|
||||||
|
Administrators can manage user accounts and permissions:
|
||||||
|
|
||||||
|
### User Operations
|
||||||
|
|
||||||
|
- **Create User**: Add new users to the system
|
||||||
|
- **Edit User**: Modify existing user details and permissions
|
||||||
|
- **Deactivate User**: Temporarily disable user accounts
|
||||||
|
- **Delete User**: Permanently remove a user account
|
||||||
|
- **Reset Password**: Force password reset for a user
|
||||||
|
|
||||||
|
### Role Management
|
||||||
|
|
||||||
|
- **View Roles**: List all available roles and their permissions
|
||||||
|
- **Create Role**: Define custom roles with specific permissions
|
||||||
|
- **Edit Role**: Modify permissions for existing roles
|
||||||
|
- **Assign Roles**: Change role assignments for users
|
||||||
|
|
||||||
|
## System Settings
|
||||||
|
|
||||||
|
The System Settings section allows customization of various system parameters:
|
||||||
|
|
||||||
|
### General Settings
|
||||||
|
|
||||||
|
- **System Name**: Customize the application name
|
||||||
|
- **Base URL**: Set the base URL for the application
|
||||||
|
- **Time Zone**: Configure the system time zone
|
||||||
|
- **Date Format**: Set the preferred date and time format
|
||||||
|
- **Default Language**: Set the default interface language
|
||||||
|
|
||||||
|
### Security Settings
|
||||||
|
|
||||||
|
- **Password Policy**: Configure password complexity requirements
|
||||||
|
- **Session Timeout**: Set the inactive session timeout period
|
||||||
|
- **Failed Login Limit**: Set thresholds for account lockouts
|
||||||
|
- **API Token Management**: Configure API token policies
|
||||||
|
|
||||||
|
### Email Settings
|
||||||
|
|
||||||
|
- **SMTP Configuration**: Set up the mail server for notifications
|
||||||
|
- **Email Templates**: Customize notification email templates
|
||||||
|
- **Notification Rules**: Configure default notification settings
|
||||||
|
|
||||||
|
### Transfer Settings
|
||||||
|
|
||||||
|
- **Concurrency Limits**: Set maximum simultaneous transfers
|
||||||
|
- **Bandwidth Limits**: Configure default bandwidth limitations
|
||||||
|
- **Temporary Storage**: Configure temp directory for transfers
|
||||||
|
- **Transfer Timeouts**: Set default timeouts for transfers
|
||||||
|
|
||||||
|
## Maintenance Mode
|
||||||
|
|
||||||
|
Administrators can put the system into maintenance mode when needed:
|
||||||
|
|
||||||
|
<!-- ### Maintenance Options
|
||||||
|
|
||||||
|
- **Enable Maintenance Mode**: Temporarily restrict access to admin users
|
||||||
|
- **Scheduled Maintenance**: Schedule maintenance windows
|
||||||
|
- **Maintenance Message**: Customize the message shown to users
|
||||||
|
- **Allow Specific IPs**: Allow specific IP addresses during maintenance -->
|
||||||
|
|
||||||
|
## Import/Export
|
||||||
|
|
||||||
|
The system provides facilities for importing and exporting configuration:
|
||||||
|
|
||||||
|
### Import/Export Features
|
||||||
|
|
||||||
|
- **Export Configurations**: Export transfer configurations as JSON
|
||||||
|
- **Import Configurations**: Import configurations from JSON files
|
||||||
|
- **Migrate Settings**: Move settings between GoMFT instances
|
||||||
|
- **Bulk Operations**: Perform operations on multiple items
|
||||||
|
|
||||||
|
## Audit Logs
|
||||||
|
|
||||||
|
For security and compliance, GoMFT maintains comprehensive audit logs:
|
||||||
|
|
||||||
|
### Audit Log Features
|
||||||
|
|
||||||
|
- **User Actions**: Records of all user-initiated actions
|
||||||
|
- **System Events**: Important system-level events
|
||||||
|
- **Authentication Events**: Login, logout, and access attempts
|
||||||
|
- **Configuration Changes**: Changes to system configuration
|
||||||
|
- **Filtering**: Filter logs by user, action type, and date range
|
||||||
|
- **Export**: Export audit logs for compliance reporting
|
||||||
|
|
||||||
|
<!-- ## Troubleshooting Tools
|
||||||
|
|
||||||
|
The Admin Tools includes several utilities for troubleshooting:
|
||||||
|
|
||||||
|
### Troubleshooting Features
|
||||||
|
|
||||||
|
- **Test Connections**: Verify connectivity to remote systems
|
||||||
|
- **Check File Permissions**: Test access to file systems
|
||||||
|
- **Debug Mode**: Enable additional logging for troubleshooting
|
||||||
|
- **Transfer Simulation**: Test transfers without moving data
|
||||||
|
- **System Check**: Run a comprehensive system check -->
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 8
|
||||||
|
title: Command Line Tools
|
||||||
|
---
|
||||||
|
|
||||||
|
GoMFT provides a command line tool called `gomftctl` that allows administrators to perform various management tasks without using the web interface. This tool is particularly useful for automation, scripting, and performing administrative tasks in environments where the web UI is not accessible.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The `gomftctl` tool is included with your GoMFT installation. You can find it in the root directory of your GoMFT installation.
|
||||||
|
|
||||||
|
If you need to build it manually, you can do so with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/gomft
|
||||||
|
go build -o gomftctl ./cmd/gomftctl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with Docker
|
||||||
|
|
||||||
|
If you're running GoMFT in a Docker container, the `gomftctl` tool is already included in the container. You can run it using the `docker exec` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Replace gomft-container with your actual container name
|
||||||
|
docker exec -it gomft-container /app/gomftctl [command] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, to view the version information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it gomft-container /app/gomftctl version
|
||||||
|
```
|
||||||
|
|
||||||
|
For commands that require stopping the application first (like key rotation), you'll need to:
|
||||||
|
|
||||||
|
1. Stop the container
|
||||||
|
2. Run the command in a new container using the same volumes
|
||||||
|
3. Restart the original container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the container
|
||||||
|
docker stop gomft-container
|
||||||
|
|
||||||
|
# Run a command using the same volumes
|
||||||
|
docker run --rm -v gomft_data:/app/data -v gomft_backups:/app/backups gomft/gomft:latest /app/gomftctl [command] [options]
|
||||||
|
|
||||||
|
# Restart the container
|
||||||
|
docker start gomft-container
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
The `gomftctl` tool provides the following commands:
|
||||||
|
|
||||||
|
### Provider Data Migration
|
||||||
|
|
||||||
|
Migrate provider data from older versions of GoMFT to the new storage provider model:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl migrate-providers [--dry-run] [--validate-only] [--force] [--backup-dir PATH] [--debug] [--auto-fill]
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--dry-run`: Simulate migration without making changes
|
||||||
|
- `--validate-only`: Only validate if migration is possible without making changes
|
||||||
|
- `--force`: Force migration even if validation fails
|
||||||
|
- `--backup-dir`: Directory to store backup data (defaults to config backup_dir)
|
||||||
|
- `--debug`: Enable debug mode with more detailed error messages
|
||||||
|
- `--auto-fill`: Automatically fill missing required fields with placeholder values
|
||||||
|
|
||||||
|
### Security Key Rotation
|
||||||
|
|
||||||
|
Generate new security keys for the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl rotate-key --type [jwt|totp|encryption] [--write]
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--type`: Type of key to rotate (required)
|
||||||
|
- `jwt`: JSON Web Token signing key
|
||||||
|
- `totp`: TOTP encryption key
|
||||||
|
- `encryption`: General encryption key used for sensitive data
|
||||||
|
- `--write`: Write the new key directly to .env file (otherwise just displays the key)
|
||||||
|
|
||||||
|
### Encryption Key Rotation
|
||||||
|
|
||||||
|
Rotate encryption keys for sensitive data stored in the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl rotate-encryption-key [--dry-run] [--batch-size SIZE] [--max-errors NUM] [--backup-dir PATH] [--skip-backup] [--old-key-env VAR] [--models MODE]
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will:
|
||||||
|
1. Create a backup of your database (unless `--skip-backup` is specified)
|
||||||
|
2. Re-encrypt all sensitive data with a new encryption key
|
||||||
|
3. Provide instructions for updating your configuration
|
||||||
|
|
||||||
|
**Important**: The application must be stopped before running this command to prevent data corruption.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--dry-run`: Simulate key rotation without making changes
|
||||||
|
- `--batch-size`: Number of records to process in each batch (default 100)
|
||||||
|
- `--max-errors`: Maximum number of errors before aborting (default 50)
|
||||||
|
- `--backup-dir`: Directory to store backup data (defaults to config backup_dir)
|
||||||
|
- `--skip-backup`: Skip database backup (not recommended)
|
||||||
|
- `--old-key-env`: Environment variable containing the old encryption key (defaults to GOMFT_ENCRYPTION_KEY)
|
||||||
|
- `--models`: Models to process (use 'auto' for automatic detection, default 'auto')
|
||||||
|
|
||||||
|
### Database Backup
|
||||||
|
|
||||||
|
Create a backup of the GoMFT database and configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl backup [--output-dir PATH]
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--output-dir`: Directory to store backup files (defaults to config backup_dir)
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
|
Commands for managing GoMFT users:
|
||||||
|
|
||||||
|
#### Create a new user
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl user create --email EMAIL --password PASSWORD [--admin]
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--email`: User email address (required)
|
||||||
|
- `--password`: User password (required)
|
||||||
|
- `--admin`: Grant admin privileges to the user
|
||||||
|
|
||||||
|
#### Reset a user's password
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl user reset-password --email EMAIL --password PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--email`: User email address (required)
|
||||||
|
- `--password`: New password (required)
|
||||||
|
|
||||||
|
#### List all users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl user list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Information
|
||||||
|
|
||||||
|
Display version information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Migrating Provider Data
|
||||||
|
|
||||||
|
To migrate provider data with a dry run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First do a dry run to see what would happen
|
||||||
|
./gomftctl migrate-providers --dry-run
|
||||||
|
|
||||||
|
# Then run the actual migration
|
||||||
|
./gomftctl migrate-providers
|
||||||
|
```
|
||||||
|
|
||||||
|
If you encounter errors due to missing required fields, you can use the auto-fill option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrate with auto-fill to handle missing required fields
|
||||||
|
./gomftctl migrate-providers --auto-fill
|
||||||
|
|
||||||
|
# For more detailed error information, add the debug flag
|
||||||
|
./gomftctl migrate-providers --auto-fill --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
When using `--auto-fill`, the system will:
|
||||||
|
1. Automatically supply placeholder values for missing required fields
|
||||||
|
2. Mark providers with "[AUTO-FILLED]" in their names
|
||||||
|
3. Log warnings about which fields were auto-filled
|
||||||
|
4. Allow you to update the correct values after migration
|
||||||
|
|
||||||
|
### Rotating JWT Secret Key
|
||||||
|
|
||||||
|
To rotate the JWT secret key and update the .env file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl rotate-key --type jwt --write
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotating Encryption Key for Sensitive Data
|
||||||
|
|
||||||
|
To rotate the encryption key used for sensitive data in the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First stop the GoMFT application
|
||||||
|
systemctl stop gomft
|
||||||
|
|
||||||
|
# Run a dry run to see what would be affected
|
||||||
|
./gomftctl rotate-encryption-key --dry-run
|
||||||
|
|
||||||
|
# Perform the actual key rotation
|
||||||
|
./gomftctl rotate-encryption-key
|
||||||
|
|
||||||
|
# Update your environment variable or .env file with the new key
|
||||||
|
# Then restart the application
|
||||||
|
systemctl start gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With Docker
|
||||||
|
|
||||||
|
To rotate encryption keys when running GoMFT in Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the container
|
||||||
|
docker stop gomft-container
|
||||||
|
|
||||||
|
# Run a dry run to see what would be affected
|
||||||
|
docker run --rm -v gomft_data:/app/data -v gomft_backups:/app/backups gomft/gomft:latest /app/gomftctl rotate-encryption-key --dry-run
|
||||||
|
|
||||||
|
# Perform the actual key rotation
|
||||||
|
docker run --rm -v gomft_data:/app/data -v gomft_backups:/app/backups gomft/gomft:latest /app/gomftctl rotate-encryption-key
|
||||||
|
|
||||||
|
# Update your environment variables in your docker-compose.yml or run command
|
||||||
|
# Then restart the container
|
||||||
|
docker start gomft-container
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating an Admin User
|
||||||
|
|
||||||
|
To create a new admin user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl user create --email admin@example.com --password secure_password --admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backing Up the Database
|
||||||
|
|
||||||
|
To create a backup of the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomftctl backup --output-dir /path/to/backup/directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using in Scripts
|
||||||
|
|
||||||
|
The `gomftctl` tool is designed to be used in scripts and automation. For example, you could create a cron job to backup the database daily:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to crontab
|
||||||
|
0 2 * * * /path/to/gomft/gomftctl backup --output-dir /path/to/backup/directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you could create a script to rotate security keys periodically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Stop the GoMFT service
|
||||||
|
systemctl stop gomft
|
||||||
|
|
||||||
|
# Rotate all security keys
|
||||||
|
/path/to/gomft/gomftctl rotate-key --type jwt --write
|
||||||
|
/path/to/gomft/gomftctl rotate-key --type totp --write
|
||||||
|
/path/to/gomft/gomftctl rotate-key --type encryption --write
|
||||||
|
|
||||||
|
# Rotate encryption key for sensitive data in the database
|
||||||
|
/path/to/gomft/gomftctl rotate-encryption-key
|
||||||
|
|
||||||
|
# Restart the GoMFT service to apply changes
|
||||||
|
systemctl restart gomft
|
||||||
|
```
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Gotify Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gotify Notifications
|
||||||
|
|
||||||
|
Gotify is a simple server for sending and receiving push notifications. GoMFT integrates with Gotify to provide real-time notifications for transfer events and system alerts.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Gotify integration allows GoMFT to:
|
||||||
|
|
||||||
|
- Send push notifications to your self-hosted Gotify server
|
||||||
|
- Customize notification priority based on event importance
|
||||||
|
- Include detailed transfer information in notifications
|
||||||
|
- Support private and secure notification delivery
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before setting up Gotify notifications in GoMFT, you need:
|
||||||
|
|
||||||
|
1. A running Gotify server (self-hosted)
|
||||||
|
2. An application token from your Gotify server
|
||||||
|
3. Network connectivity between GoMFT and the Gotify server
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global Gotify Settings
|
||||||
|
|
||||||
|
To configure Gotify notifications:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notification Services** > **Add New** > **Gotify**
|
||||||
|
2. Configure the following settings:
|
||||||
|
- **Gotify Server URL**: The URL of your Gotify server (e.g., `https://gotify.example.com`)
|
||||||
|
- **Application Token**: The token for your GoMFT application in Gotify
|
||||||
|
- **Default Priority**: The default priority level for notifications (1-10)
|
||||||
|
- **Verify SSL**: Whether to verify SSL certificates (recommended for production)
|
||||||
|
|
||||||
|
### Testing Gotify Connection
|
||||||
|
|
||||||
|
After configuring your Gotify settings:
|
||||||
|
|
||||||
|
1. Click **Test Connection** to verify connectivity with your Gotify server
|
||||||
|
2. Click **Send Test Notification** to send a test message
|
||||||
|
|
||||||
|
## Notification Content
|
||||||
|
|
||||||
|
### Priority Levels
|
||||||
|
|
||||||
|
Gotify uses numeric priority levels that GoMFT leverages for different event types:
|
||||||
|
|
||||||
|
| Priority | Usage in GoMFT |
|
||||||
|
|----------|----------------|
|
||||||
|
| 1-3 | Low priority: successful transfers, routine events |
|
||||||
|
| 4-7 | Medium priority: warnings, transfers with issues |
|
||||||
|
| 8-10 | High priority: failed transfers, critical system issues |
|
||||||
|
|
||||||
|
### Example Notifications
|
||||||
|
|
||||||
|
GoMFT sends structured notifications with helpful information:
|
||||||
|
|
||||||
|
#### Successful Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Completed: Daily Backup
|
||||||
|
Message: Successfully transferred 123 files (1.45 GB) in 2:15
|
||||||
|
Priority: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Failed Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Failed: Daily Backup
|
||||||
|
Message: Error: Connection refused to destination server
|
||||||
|
Files processed: 45/123
|
||||||
|
Size transferred: 0.5/1.45 GB
|
||||||
|
Priority: 8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Connection Refused**: Ensure the Gotify server URL is correct and accessible
|
||||||
|
- **Authentication Failed**: Verify the Application Token is correct
|
||||||
|
- **SSL Certificate Errors**: Check the Verify SSL setting and certificate validity
|
||||||
|
|
||||||
|
### Gotify Logs
|
||||||
|
|
||||||
|
To troubleshoot notification issues:
|
||||||
|
|
||||||
|
1. Check the GoMFT logs: **Admin Tools** > **Logs** > filter for "gotify"
|
||||||
|
2. Review the Gotify server logs for any errors
|
||||||
|
3. Verify network connectivity between GoMFT and the Gotify server
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use HTTPS** for your Gotify server to ensure secure communication
|
||||||
|
- **Set Appropriate Priorities** to differentiate between routine and critical notifications
|
||||||
|
- **Use Client Applications** on your devices to receive Gotify notifications
|
||||||
|
- **Set Up Multiple Notification Methods** for critical events
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Notifications Overview
|
||||||
|
---
|
||||||
|
|
||||||
|
# Notifications Overview
|
||||||
|
|
||||||
|
GoMFT provides a comprehensive notification system to keep you informed about important events in your file transfer workflows. This page provides an overview of the notification system and the different notification types available.
|
||||||
|
|
||||||
|
## Notification System
|
||||||
|
|
||||||
|
The notification system in GoMFT is designed to be:
|
||||||
|
|
||||||
|
- **Flexible**: Choose from multiple notification channels
|
||||||
|
- **Configurable**: Set up different notifications for different events
|
||||||
|
- **Reliable**: Ensure critical events are always reported
|
||||||
|
- **Secure**: Protect sensitive information in notifications
|
||||||
|
|
||||||
|
## Notification Triggers
|
||||||
|
|
||||||
|
Notifications can be triggered by various events in GoMFT:
|
||||||
|
|
||||||
|
### Transfer-Related Events
|
||||||
|
|
||||||
|
- **Transfer Completion**: When a file transfer is successfully completed
|
||||||
|
- **Transfer Failure**: When a file transfer fails for any reason
|
||||||
|
- **Transfer Start**: When a file transfer begins
|
||||||
|
- **Transfer Threshold**: When a transfer exceeds a defined duration threshold
|
||||||
|
|
||||||
|
### Schedule-Related Events
|
||||||
|
|
||||||
|
- **Schedule Execution**: When a scheduled task runs
|
||||||
|
- **Schedule Failure**: When a scheduled task fails to run
|
||||||
|
- **Schedule Creation/Modification**: When schedules are created or modified
|
||||||
|
|
||||||
|
<!-- ### System Events
|
||||||
|
|
||||||
|
- **System Warnings**: Alerts about system resource usage (disk space, memory, etc.)
|
||||||
|
- **Service Status Changes**: When system services change state
|
||||||
|
- **Authentication Events**: Failed login attempts or other security events
|
||||||
|
- **Database Events**: Database backup completion, migration, or issues -->
|
||||||
|
|
||||||
|
## Notification Types
|
||||||
|
|
||||||
|
GoMFT supports multiple notification types to ensure you can receive alerts through your preferred channels:
|
||||||
|
|
||||||
|
|
||||||
|
### Webhook Notifications
|
||||||
|
|
||||||
|
Send HTTP requests to external systems or services when events occur. Features include:
|
||||||
|
- Configurable HTTP methods (POST, PUT, PATCH)
|
||||||
|
- JSON or XML payload formats
|
||||||
|
- Support for authentication
|
||||||
|
- Customizable retry strategy for improved reliability
|
||||||
|
[Learn more about Webhook Notifications](./webhook-notifications)
|
||||||
|
|
||||||
|
### Mobile Push Notifications
|
||||||
|
|
||||||
|
Receive notifications directly on your mobile devices:
|
||||||
|
|
||||||
|
#### Ntfy Notifications
|
||||||
|
- Simple HTTP-based push notifications to phones and desktops
|
||||||
|
- Customizable priority levels and notification tags
|
||||||
|
- Support for self-hosted or cloud-based ntfy servers
|
||||||
|
[Learn more about Ntfy Notifications](./ntfy-notifications)
|
||||||
|
|
||||||
|
#### Pushover Notifications
|
||||||
|
- Real-time push notifications to all your devices
|
||||||
|
- Priority levels for urgent notifications
|
||||||
|
- Custom sounds and delivery options
|
||||||
|
[Learn more about Pushover Notifications](./pushover-notifications)
|
||||||
|
|
||||||
|
#### Pushbullet Notifications
|
||||||
|
- Cross-platform notifications across all your devices
|
||||||
|
- Optional end-to-end encryption
|
||||||
|
- Support for notification mirroring
|
||||||
|
[Learn more about Pushbullet Notifications](./pushbullet-notifications)
|
||||||
|
|
||||||
|
#### Gotify Notifications
|
||||||
|
- Self-hosted push notification service
|
||||||
|
- Customizable priority levels
|
||||||
|
- Private and secure notification delivery
|
||||||
|
[Learn more about Gotify Notifications](./gotify-notifications)
|
||||||
|
|
||||||
|
## Notification Templates
|
||||||
|
|
||||||
|
Each notification type uses customizable templates to format the notification content. Templates support variables that are replaced with actual values when the notification is sent.
|
||||||
|
|
||||||
|
Common template variables include:
|
||||||
|
|
||||||
|
- `{{transfer_name}}`: Name of the transfer
|
||||||
|
- `{{transfer_status}}`: Status of the transfer (success, failure, etc.)
|
||||||
|
- `{{start_time}}`: When the transfer started
|
||||||
|
- `{{end_time}}`: When the transfer completed
|
||||||
|
- `{{duration}}`: How long the transfer took
|
||||||
|
- `{{total_files}}`: Number of files transferred
|
||||||
|
- `{{total_size}}`: Total size of transferred data
|
||||||
|
- `{{error_message}}`: Detailed error information (for failures)
|
||||||
|
|
||||||
|
## Notification Management
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Notifications are configured at multiple levels:
|
||||||
|
|
||||||
|
1. **Global Level**: Default notification settings for all transfers
|
||||||
|
2. **Transfer Level**: Specific notification settings for individual transfers
|
||||||
|
3. **Schedule Level**: Notification settings for scheduled transfers
|
||||||
|
|
||||||
|
### Notification History
|
||||||
|
|
||||||
|
GoMFT maintains a history of sent notifications, allowing you to:
|
||||||
|
|
||||||
|
- Review past notifications
|
||||||
|
- Verify notification delivery
|
||||||
|
- Resend notifications if needed
|
||||||
|
- Audit notification patterns
|
||||||
|
|
||||||
|
## Getting Started with Notifications
|
||||||
|
|
||||||
|
To start using GoMFT notifications:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notifications**
|
||||||
|
2. Configure your preferred notification channels
|
||||||
|
3. Test each notification channel
|
||||||
|
4. Apply notifications to specific transfers or schedules
|
||||||
|
|
||||||
|
For specific notification types, refer to the corresponding documentation pages in this section.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
title: Ntfy Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ntfy Notifications
|
||||||
|
|
||||||
|
Ntfy is a simple HTTP-based pub-sub notification service that allows you to send push notifications to your phone or desktop. GoMFT seamlessly integrates with Ntfy to deliver notifications about your file transfers and system events.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Ntfy integration in GoMFT enables:
|
||||||
|
|
||||||
|
- Push notifications to mobile devices and desktops
|
||||||
|
- Choice between public ntfy.sh service or self-hosted Ntfy server
|
||||||
|
- Customizable notification topics, priorities, and tags
|
||||||
|
- Support for notification actions and attachments
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before configuring Ntfy notifications in GoMFT, you should:
|
||||||
|
|
||||||
|
1. Install the Ntfy app on your devices (available for Android, iOS, and desktop)
|
||||||
|
2. Subscribe to your chosen topic in the Ntfy app
|
||||||
|
3. Optionally set up your own Ntfy server for increased privacy
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global Ntfy Settings
|
||||||
|
|
||||||
|
To configure Ntfy notifications in GoMFT:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notification Services** > **Add New** > **Ntfy**
|
||||||
|
2. Configure the following settings:
|
||||||
|
- **Ntfy Server URL**: The URL of the Ntfy server (default: `https://ntfy.sh`)
|
||||||
|
- **Default Topic**: The notification topic your devices are subscribed to
|
||||||
|
- **Default Priority**: Priority level for notifications (1-5)
|
||||||
|
- **Authentication**: Access token or username/password if required
|
||||||
|
- **Default Tags**: Icon tags for different notification types
|
||||||
|
|
||||||
|
### Testing Ntfy Connection
|
||||||
|
|
||||||
|
After configuring your Ntfy settings:
|
||||||
|
|
||||||
|
1. Click **Send Test Notification** to send a test notification to your devices
|
||||||
|
|
||||||
|
## Notification Content
|
||||||
|
|
||||||
|
### Priority Levels
|
||||||
|
|
||||||
|
Ntfy supports five priority levels that GoMFT uses effectively:
|
||||||
|
|
||||||
|
| Priority | Level | Usage in GoMFT |
|
||||||
|
|----------|-------|----------------|
|
||||||
|
| 1 | Min | Background information, debug notifications |
|
||||||
|
| 2 | Low | Successful transfers, routine events |
|
||||||
|
| 3 | Default | Standard notifications, warnings |
|
||||||
|
| 4 | High | Transfer failures, important alerts |
|
||||||
|
| 5 | Max | Critical system issues, emergency alerts |
|
||||||
|
|
||||||
|
### Notification Tags
|
||||||
|
|
||||||
|
GoMFT uses meaningful tags in Ntfy notifications to provide visual cues:
|
||||||
|
|
||||||
|
| Tag | Usage |
|
||||||
|
|-----|-------|
|
||||||
|
| `✅` | Successful transfers |
|
||||||
|
| `❌` | Failed transfers |
|
||||||
|
| `⚠️` | Warnings or transfers with issues |
|
||||||
|
| `🔄` | Transfer in progress |
|
||||||
|
| `🔍` | Monitoring events |
|
||||||
|
| `⚙️` | System events |
|
||||||
|
|
||||||
|
### Example Notifications
|
||||||
|
|
||||||
|
GoMFT sends structured notifications with helpful information:
|
||||||
|
|
||||||
|
#### Successful Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Completed: Daily Backup
|
||||||
|
Message: Successfully transferred 123 files (1.45 GB) in 2:15
|
||||||
|
Priority: 2 (Low)
|
||||||
|
Tags: ✅,📁
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Failed Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Failed: Daily Backup
|
||||||
|
Message: Error: Connection refused to destination server
|
||||||
|
Files processed: 45/123
|
||||||
|
Size transferred: 0.5/1.45 GB
|
||||||
|
Priority: 4 (High)
|
||||||
|
Tags: ❌,📁
|
||||||
|
Click action: Open GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Custom Templates
|
||||||
|
|
||||||
|
Customize notification content with templates:
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: {{event_type}}: {{transfer_name}}
|
||||||
|
Message: {{status}} - {{files_transferred}} files ({{total_size}}) in {{duration}}
|
||||||
|
Priority: {% if status == "failed" %}4{% else %}2{% endif %}
|
||||||
|
Tags: {% if status == "success" %}✅{% else %}❌{% endif %},📁
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-Hosting Ntfy
|
||||||
|
|
||||||
|
For enhanced privacy and control, you can self-host your own Ntfy server:
|
||||||
|
|
||||||
|
1. Follow the [Ntfy self-hosting guide](https://docs.ntfy.sh/install/)
|
||||||
|
2. Update your GoMFT configuration to point to your self-hosted server
|
||||||
|
3. Configure authentication as needed
|
||||||
|
|
||||||
|
Example configuration for self-hosted Ntfy:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ntfy:
|
||||||
|
server_url: https://ntfy.example.com
|
||||||
|
default_topic: gomft
|
||||||
|
authentication:
|
||||||
|
type: basic
|
||||||
|
username: ${NTFY_USERNAME}
|
||||||
|
password: ${NTFY_PASSWORD}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Notifications Not Arriving**: Verify you've subscribed to the correct topic
|
||||||
|
- **Authentication Errors**: Check credentials and authentication method
|
||||||
|
- **Connection Issues**: Ensure the Ntfy server is accessible from GoMFT
|
||||||
|
- **App Configuration**: Verify notification settings in your Ntfy app
|
||||||
|
|
||||||
|
### Ntfy Logs
|
||||||
|
|
||||||
|
To troubleshoot notification issues:
|
||||||
|
|
||||||
|
1. Check the GoMFT logs: **Administration** > **Log Viewer** > filter for "ntfy"
|
||||||
|
2. If self-hosting, check your Ntfy server logs
|
||||||
|
3. Verify your device has properly functioning notifications
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use Unique Topics** to prevent unauthorized notifications
|
||||||
|
- **Set Appropriate Priorities** based on event importance
|
||||||
|
- **Consider Self-Hosting** for sensitive environments
|
||||||
|
- **Keep Topic Names Secret** as they act like passwords
|
||||||
|
- **Set Up Multiple Notification Methods** for critical systems
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 5
|
||||||
|
title: Pushbullet Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pushbullet Notifications
|
||||||
|
|
||||||
|
Pushbullet is a cross-platform notification service that allows you to receive notifications on multiple devices. GoMFT integrates with Pushbullet to deliver timely notifications about your file transfers and system events.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Pushbullet integration in GoMFT offers:
|
||||||
|
|
||||||
|
- Cross-platform notifications across your devices (Android, iOS, Chrome, Firefox, etc.)
|
||||||
|
- Option to send to all devices or specific devices
|
||||||
|
- Rich notification content with transfer details
|
||||||
|
- Support for notification mirroring between devices
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before configuring Pushbullet notifications in GoMFT, you need:
|
||||||
|
|
||||||
|
1. A Pushbullet account
|
||||||
|
2. Pushbullet API access token
|
||||||
|
3. Pushbullet app installed on your devices
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global Pushbullet Settings
|
||||||
|
|
||||||
|
To configure Pushbullet notifications:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notification Services** > **Add New** > **Pushbullet**
|
||||||
|
2. Configure the following settings:
|
||||||
|
- **API Token**: Your Pushbullet access token
|
||||||
|
- **Default Device**: The device identifier to send notifications to (optional)
|
||||||
|
- **Default Type**: "Note" (default) or "Link"
|
||||||
|
|
||||||
|
### Getting Your Pushbullet API Token
|
||||||
|
|
||||||
|
1. Log in to your Pushbullet account at [pushbullet.com](https://www.pushbullet.com/)
|
||||||
|
2. Go to **Settings** > **Account**
|
||||||
|
3. In the **Access Tokens** section, click **Create Access Token**
|
||||||
|
4. Copy the generated token and paste it into GoMFT
|
||||||
|
|
||||||
|
### Testing Pushbullet Connection
|
||||||
|
|
||||||
|
After configuring your Pushbullet settings:
|
||||||
|
|
||||||
|
1. Click **Send Test Notification** to send a test notification to your devices
|
||||||
|
|
||||||
|
## Notification Content
|
||||||
|
|
||||||
|
### Notification Types
|
||||||
|
|
||||||
|
GoMFT supports two types of Pushbullet notifications:
|
||||||
|
|
||||||
|
#### Note Type
|
||||||
|
|
||||||
|
Simple notifications with a title and body:
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Complete: Daily Backup
|
||||||
|
Body: Successfully transferred 123 files (1.45 GB) in 2:15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Link Type
|
||||||
|
|
||||||
|
Notifications that include a link to the GoMFT interface:
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Failed: Daily Backup
|
||||||
|
Body: Error: Connection refused to destination server
|
||||||
|
URL: https://gomft.example.com/transfers/123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Notifications
|
||||||
|
|
||||||
|
#### Successful Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Complete: Daily Backup
|
||||||
|
Body: Transfer completed successfully at 2023-09-15 14:22:33
|
||||||
|
Files: 123
|
||||||
|
Size: 1.45 GB
|
||||||
|
Duration: 00:02:15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Failed Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Failed: Daily Backup
|
||||||
|
Body: Transfer failed with error: Connection refused
|
||||||
|
Files Processed: 45/123
|
||||||
|
Size Transferred: 0.5/1.45 GB
|
||||||
|
Duration: 00:01:05
|
||||||
|
Error: Failed to connect to destination server
|
||||||
|
URL: https://gomft.example.com/transfers/123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **API Token Errors**: Verify your Pushbullet API token is correct
|
||||||
|
- **No Notifications Arriving**: Check device connectivity and Pushbullet app settings
|
||||||
|
- **Rate Limiting**: Pushbullet has API rate limits; spread out notification frequency
|
||||||
|
- **Device Selection Issues**: Verify device identifiers if targeting specific devices
|
||||||
|
|
||||||
|
### Pushbullet Logs
|
||||||
|
|
||||||
|
To troubleshoot notification issues:
|
||||||
|
|
||||||
|
1. Check the GoMFT logs: **Administration** > **Log Viewer** > filter for "pushbullet"
|
||||||
|
2. Review the Pushbullet account activity in your Pushbullet account
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Secure Your API Token**: Treat your Pushbullet API token as sensitive information
|
||||||
|
- **Group Related Notifications** to avoid notification fatigue
|
||||||
|
- **Include Action Links** for quick access to relevant GoMFT pages
|
||||||
|
- **Set Up Multiple Notification Methods** for critical systems
|
||||||
|
|
||||||
|
## Pushbullet Alternatives
|
||||||
|
|
||||||
|
If you encounter limitations with Pushbullet, GoMFT also supports:
|
||||||
|
|
||||||
|
- [Email Notifications](./email-notifications)
|
||||||
|
- [Gotify](./gotify-notifications)
|
||||||
|
- [Ntfy](./ntfy-notifications)
|
||||||
|
- [Pushover](./pushover-notifications)
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 6
|
||||||
|
title: Pushover Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pushover Notifications
|
||||||
|
|
||||||
|
Pushover is a simple notification service that makes it easy to send real-time notifications to your Android and iOS devices, as well as desktop computers. GoMFT integrates with Pushover to deliver instant notifications about your file transfers and system events.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Pushover integration in GoMFT provides:
|
||||||
|
|
||||||
|
- Real-time push notifications to mobile devices and desktops
|
||||||
|
- Prioritized notifications with different sounds and attention levels
|
||||||
|
- Customizable notification content with detailed transfer information
|
||||||
|
- Support for notification grouping by device or application
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before configuring Pushover notifications in GoMFT, you need:
|
||||||
|
|
||||||
|
1. A [Pushover account](https://pushover.net/)
|
||||||
|
2. Your Pushover user key
|
||||||
|
3. A registered Pushover application (API token)
|
||||||
|
4. Pushover app installed on your devices
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Registering a GoMFT Application in Pushover
|
||||||
|
|
||||||
|
1. Log in to your Pushover account at [pushover.net](https://pushover.net/)
|
||||||
|
2. Go to [Your Applications](https://pushover.net/apps/build)
|
||||||
|
3. Create a new application:
|
||||||
|
- **Name**: GoMFT
|
||||||
|
- **Type**: Application
|
||||||
|
- **Description**: GoMFT File Transfer Notifications
|
||||||
|
- **URL**: Your GoMFT instance URL (optional)
|
||||||
|
- **Icon**: Upload a custom icon (optional)
|
||||||
|
4. After creation, you'll receive an **API Token/Key** for your application
|
||||||
|
|
||||||
|
### Global Pushover Settings
|
||||||
|
|
||||||
|
To configure Pushover notifications in GoMFT:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notification Services** > **Add New** > **Pushover**
|
||||||
|
2. Configure the following settings:
|
||||||
|
- **User Key**: Your Pushover user key
|
||||||
|
- **API Token**: Your GoMFT application's API token
|
||||||
|
- **Default Priority**: Default priority level (-2 to 2)
|
||||||
|
- **Default Sound**: Sound for notifications
|
||||||
|
- **Default Device**: Specific device or blank for all devices
|
||||||
|
|
||||||
|
### Testing Pushover Connection
|
||||||
|
|
||||||
|
After configuring your Pushover settings:
|
||||||
|
|
||||||
|
1. Click **Send Test Notification** to send a test notification to your devices
|
||||||
|
|
||||||
|
## Notification Content
|
||||||
|
|
||||||
|
### Priority Levels
|
||||||
|
|
||||||
|
Pushover supports different priority levels that GoMFT uses effectively:
|
||||||
|
|
||||||
|
| Priority | Level | Usage in GoMFT |
|
||||||
|
|----------|-------|----------------|
|
||||||
|
| -2 | Lowest | Silent transfer logs, debugging info |
|
||||||
|
| -1 | Low | Successful transfers, routine events |
|
||||||
|
| 0 | Normal | Standard notifications |
|
||||||
|
| 1 | High | Transfer failures, important alerts |
|
||||||
|
| 2 | Emergency | Critical system issues |
|
||||||
|
|
||||||
|
Note: Emergency priority (2) notifications will repeat until acknowledged by the user.
|
||||||
|
|
||||||
|
### Example Notifications
|
||||||
|
|
||||||
|
#### Successful Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Complete: Daily Backup
|
||||||
|
Message: Successfully transferred 123 files (1.45 GB) in 2:15
|
||||||
|
Priority: Normal (0)
|
||||||
|
Sound: pushover
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Failed Transfer
|
||||||
|
|
||||||
|
```
|
||||||
|
Title: Transfer Failed: Daily Backup
|
||||||
|
Message: Error: Connection refused to destination server
|
||||||
|
Files: 45/123 processed
|
||||||
|
Size: 0.5/1.45 GB transferred
|
||||||
|
Error: Failed to connect to destination server
|
||||||
|
Priority: High (1)
|
||||||
|
Sound: siren
|
||||||
|
URL: https://gomft.example.com/transfers/123
|
||||||
|
URL Title: View Transfer Details
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Incorrect API Token/User Key**: Verify your Pushover credentials
|
||||||
|
- **Message Rate Limiting**: Pushover has monthly message limits for free accounts
|
||||||
|
- **Device Not Receiving**: Check device registration and network connectivity
|
||||||
|
- **Emergency Notifications**: Verify retry/expire settings for emergency priority
|
||||||
|
|
||||||
|
### Pushover Logs
|
||||||
|
|
||||||
|
To troubleshoot notification issues:
|
||||||
|
|
||||||
|
1. Check the GoMFT logs: **Admin Tools** > **Logs** > filter for "pushover"
|
||||||
|
2. Review your Pushover account's message history
|
||||||
|
3. Check your device's Pushover app settings
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use Appropriate Priority Levels** based on event importance
|
||||||
|
- **Reserve Emergency Priority** for truly critical issues
|
||||||
|
- **Group Related Notifications** when possible
|
||||||
|
- **Include Action URLs** for immediate access to relevant information
|
||||||
|
- **Consider Sound Selection** based on notification importance
|
||||||
|
- **Set Up Multiple Notification Methods** for critical systems
|
||||||
|
|
||||||
|
## Pushover vs. Other Notification Systems
|
||||||
|
|
||||||
|
| Feature | Pushover | Email | Gotify | Ntfy | Pushbullet |
|
||||||
|
|---------|----------|-------|--------|------|------------|
|
||||||
|
| Cost | Paid (one-time) | Free | Free | Free | Free/Paid |
|
||||||
|
| Self-hosting | No | Varies | Yes | Yes | No |
|
||||||
|
| Priority levels | Yes | No | Yes | Yes | No |
|
||||||
|
| Acknowledgment | Yes | No | No | No | No |
|
||||||
|
| Sound options | Yes | No | No | Limited | No |
|
||||||
|
| Delivery guarantee | High | Varies | Good | Good | Good |
|
||||||
|
| Device support | iOS, Android, Desktop | All | All | All | iOS, Android, Desktop |
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Webhook Notifications
|
||||||
|
---
|
||||||
|
|
||||||
|
# Webhook Notifications
|
||||||
|
|
||||||
|
Webhook notifications in GoMFT provide a powerful way to integrate with external systems by sending HTTP requests when events occur. This enables automation workflows and integration with your existing tools and services.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Webhooks allow GoMFT to:
|
||||||
|
|
||||||
|
- Send real-time notifications to external systems
|
||||||
|
- Trigger automated workflows in third-party applications
|
||||||
|
- Integrate with custom applications or services
|
||||||
|
- Provide machine-readable event data (JSON or XML)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global Webhook Settings
|
||||||
|
|
||||||
|
To configure webhook notifications:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Notification Services** > **Webhooks**
|
||||||
|
2. Configure the following settings:
|
||||||
|
- **Default Webhook URL**: The base URL for webhook requests
|
||||||
|
- **HTTP Method**: POST (default), PUT, or PATCH
|
||||||
|
- **Content Type**: application/json (default), application/xml, or custom
|
||||||
|
- **Authentication**: None, Basic Auth, API Key, or Bearer Token
|
||||||
|
- **Retry Strategy**: Number of retries and delay between retries
|
||||||
|
- **Timeout**: Maximum wait time for responses
|
||||||
|
|
||||||
|
### Testing Webhook Connectivity
|
||||||
|
|
||||||
|
After configuring your webhook settings:
|
||||||
|
|
||||||
|
1. Click **Test Connection** to send a test webhook request
|
||||||
|
2. Review the response status and body from the server
|
||||||
|
|
||||||
|
## Webhook Payload
|
||||||
|
|
||||||
|
### Default JSON Payload
|
||||||
|
|
||||||
|
By default, GoMFT sends a JSON payload with information about the event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "transfer_completed",
|
||||||
|
"timestamp": "2023-09-15T14:22:33Z",
|
||||||
|
"transfer": {
|
||||||
|
"id": "transfer-123",
|
||||||
|
"name": "Daily Backup",
|
||||||
|
"source": "local-server",
|
||||||
|
"destination": "cloud-storage",
|
||||||
|
"status": "success",
|
||||||
|
"start_time": "2023-09-15T14:20:01Z",
|
||||||
|
"end_time": "2023-09-15T14:22:33Z",
|
||||||
|
"duration_seconds": 152,
|
||||||
|
"files_transferred": 258,
|
||||||
|
"total_size_bytes": 1073741824,
|
||||||
|
"transfer_rate_bytes_per_second": 7064091
|
||||||
|
},
|
||||||
|
"schedule": {
|
||||||
|
"id": "schedule-456",
|
||||||
|
"name": "Daily Backup Schedule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For failure events, additional error information is included:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "transfer_failed",
|
||||||
|
"timestamp": "2023-09-15T14:22:33Z",
|
||||||
|
"transfer": {
|
||||||
|
"id": "transfer-123",
|
||||||
|
"name": "Daily Backup",
|
||||||
|
"source": "local-server",
|
||||||
|
"destination": "cloud-storage",
|
||||||
|
"status": "failed",
|
||||||
|
"start_time": "2023-09-15T14:20:01Z",
|
||||||
|
"end_time": "2023-09-15T14:22:33Z",
|
||||||
|
"duration_seconds": 152,
|
||||||
|
"error": {
|
||||||
|
"code": "CONNECTION_ERROR",
|
||||||
|
"message": "Failed to connect to destination: Connection timed out",
|
||||||
|
"details": "TCP connection to cloud-storage:22 timed out after 60 seconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Payload Templates
|
||||||
|
|
||||||
|
You can customize the webhook payload using templates:
|
||||||
|
|
||||||
|
1. Navigate to the webhook configuration
|
||||||
|
2. Switch from **Default Payload** to **Custom Payload**
|
||||||
|
3. Edit the JSON or XML template
|
||||||
|
|
||||||
|
Example custom template:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"type": "{{event_type}}",
|
||||||
|
"system": "GoMFT",
|
||||||
|
"environment": "{{environment}}",
|
||||||
|
"details": {
|
||||||
|
"transfer_name": "{{transfer_name}}",
|
||||||
|
"status": "{{status}}",
|
||||||
|
"time": "{{timestamp}}",
|
||||||
|
"size_mb": "{{total_size_mb}}"
|
||||||
|
}{% if status == "failed" %},
|
||||||
|
"error": "{{error_message}}"{% endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **Connection Refused**: Check network connectivity and firewall rules
|
||||||
|
- **Authentication Failed**: Verify credentials and authentication method
|
||||||
|
- **Timeout Issues**: Increase timeout settings for slow endpoints
|
||||||
|
- **Invalid Payload**: Validate your custom template syntax
|
||||||
|
- **HTTP Error Codes**: Check destination service logs for details
|
||||||
|
|
||||||
|
### Webhook Logs
|
||||||
|
|
||||||
|
GoMFT logs all webhook attempts:
|
||||||
|
|
||||||
|
1. Navigate to **Administartion** > **Log Viewer**
|
||||||
|
2. Filter for "webhook" to see relevant log entries
|
||||||
|
3. Review request and response details for troubleshooting
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use HTTPS** for all webhook endpoints
|
||||||
|
- **Implement Retries** for important notifications
|
||||||
|
- **Monitor Webhook Deliveries** to ensure reliability
|
||||||
|
- **Set Up Fallback Notification Methods** for critical transfers
|
||||||
|
- **Validate Webhook Payloads** on the receiving end
|
||||||
|
- **Keep Webhook Processing Fast** to avoid timeouts
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Connections
|
||||||
|
---
|
||||||
|
|
||||||
|
# Connection Management
|
||||||
|
|
||||||
|
Connections in GoMFT are configurations that define how to access different storage systems. Before you can transfer files, you need to set up connections for your source and destination systems.
|
||||||
|
|
||||||
|
> **Note**: GoMFT now supports the Storage Provider feature, which allows you to create reusable connection profiles with securely stored credentials. For detailed information, see the [Storage Providers](/docs/user-guides/storage-provider-guide) guide.
|
||||||
|
|
||||||
|
## Supported Connection Types
|
||||||
|
|
||||||
|
GoMFT leverages rclone as its transfer engine, supporting a wide range of storage systems:
|
||||||
|
|
||||||
|
### Cloud Storage
|
||||||
|
|
||||||
|
- **Amazon S3**: Amazon's object storage service
|
||||||
|
- **Google Cloud Storage**: Google's object storage service
|
||||||
|
- **Backblaze B2**: Affordable cloud object storage
|
||||||
|
- **Wasabi**: Hot cloud storage
|
||||||
|
|
||||||
|
### File Transfer Protocols
|
||||||
|
|
||||||
|
- **FTP**: File Transfer Protocol
|
||||||
|
- **SFTP**: SSH File Transfer Protocol
|
||||||
|
- **WebDAV**: Web Distributed Authoring and Versioning
|
||||||
|
|
||||||
|
### Local Storage
|
||||||
|
|
||||||
|
- **Local Disk**: Files on the server running GoMFT
|
||||||
|
- **SMB/CIFS**: Windows file sharing
|
||||||
|
|
||||||
|
## Creating a Connection
|
||||||
|
|
||||||
|
To create a new connection:
|
||||||
|
|
||||||
|
1. Navigate to the **Transfer Configuration** section in the sidebar
|
||||||
|
2. Click **+ New Configuration**
|
||||||
|
3. Select the connection type from the dropdown menu
|
||||||
|
4. Fill in the required details for your selected type
|
||||||
|
5. Click **Test Source/Destination** to verify the connection works
|
||||||
|
6. Click **Create Configuration** to store the configuration
|
||||||
|
|
||||||
|
## Connection Configuration Fields
|
||||||
|
|
||||||
|
Different connection types require different configuration fields. Here are some common examples:
|
||||||
|
|
||||||
|
### Amazon S3 Connection
|
||||||
|
|
||||||
|
- **Name**: A descriptive name for the connection
|
||||||
|
- **Access Key ID**: AWS access key
|
||||||
|
- **Secret Access Key**: AWS secret key
|
||||||
|
- **Region**: AWS region (e.g., us-east-1)
|
||||||
|
- **Endpoint**: Optional custom endpoint for S3-compatible services
|
||||||
|
- **Bucket**: Default bucket to use (optional)
|
||||||
|
- **Path Prefix**: Default path prefix within the bucket (optional)
|
||||||
|
|
||||||
|
### SFTP Connection
|
||||||
|
|
||||||
|
- **Name**: A descriptive name for the connection
|
||||||
|
- **Host**: Server hostname or IP address
|
||||||
|
- **Port**: Server port (usually 22)
|
||||||
|
- **Username**: SFTP username
|
||||||
|
- **Authentication Method**: Password or SSH Key
|
||||||
|
- **Password**: User password (if using password authentication)
|
||||||
|
- **SSH Key**: Private SSH key (if using key authentication)
|
||||||
|
- **SSH Key Passphrase**: Passphrase for SSH key (if applicable)
|
||||||
|
|
||||||
|
### Local Storage Connection
|
||||||
|
|
||||||
|
- **Name**: A descriptive name for the connection
|
||||||
|
- **Path**: Base path on the local filesystem
|
||||||
|
|
||||||
|
## Connection Security
|
||||||
|
|
||||||
|
GoMFT follows best practices for handling connection credentials:
|
||||||
|
|
||||||
|
- **Encryption**: All sensitive credentials are encrypted at rest using AES-256 encryption
|
||||||
|
- **Access Control**: Connections are protected by user permissions
|
||||||
|
- **Masked Values**: Passwords and secret keys are masked in the UI
|
||||||
|
- **Key Management**: SSH keys and other credentials are securely stored
|
||||||
|
- **Centralized Management**: With the Storage Provider feature, credentials can be managed in one place and reused across multiple transfers
|
||||||
|
|
||||||
|
## Managing Connections
|
||||||
|
|
||||||
|
You can manage connections either through traditional transfer configurations or using the new Storage Provider feature.
|
||||||
|
|
||||||
|
### Viewing Connections
|
||||||
|
|
||||||
|
#### Traditional Connections
|
||||||
|
|
||||||
|
The **Transfer Configurations** page displays all configured connections with:
|
||||||
|
- Configuration name
|
||||||
|
- Configuration type
|
||||||
|
- Last updated date
|
||||||
|
|
||||||
|
#### Storage Providers
|
||||||
|
|
||||||
|
Alternatively, you can use the new Storage Provider feature to manage your connections:
|
||||||
|
1. Navigate to the **Storage Providers** section in the left sidebar
|
||||||
|
2. View a list of all storage providers you have created
|
||||||
|
3. Each provider shows name, type, and creation date
|
||||||
|
|
||||||
|
### Editing Transfer Confirgurations
|
||||||
|
|
||||||
|
To edit an existing connection:
|
||||||
|
1. Navigate to the **Transfer Confiruations** section
|
||||||
|
2. Find the config you want to edit
|
||||||
|
3. Click the **Edit** button
|
||||||
|
4. Modify the config details
|
||||||
|
5. Test the updated configuration
|
||||||
|
6. Save your changes
|
||||||
|
|
||||||
|
### Deleting Transfer Configurations
|
||||||
|
|
||||||
|
To delete a connection:
|
||||||
|
1. Navigate to the **Transfer Configurations** section
|
||||||
|
2. Find the config you want to delete
|
||||||
|
3. Click the **Delete** button
|
||||||
|
4. Confirm the deletion
|
||||||
|
|
||||||
|
**Note**: You cannot delete connections that are in use by active transfers or schedules.
|
||||||
|
|
||||||
|
## Testing Transfer Configurations
|
||||||
|
|
||||||
|
GoMFT includes a configuration testing feature to verify connectivity:
|
||||||
|
|
||||||
|
1. After entering details for a configuration, click **Test Source/Destination**
|
||||||
|
2. GoMFT will attempt to authenticate with the remote system
|
||||||
|
3. For file storage, it will also verify read/write permissions
|
||||||
|
4. Results will display showing success or failure details
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use descriptive names** for connections to easily identify them
|
||||||
|
- **Test connections regularly** to ensure they still work
|
||||||
|
- **Rotate credentials** periodically for enhanced security
|
||||||
|
- **Use service accounts** rather than personal accounts when possible
|
||||||
|
- **Document connection details** in the description field
|
||||||
|
- **Use the minimal required permissions** for enhanced security
|
||||||
|
- **Organize connections** using consistent naming conventions
|
||||||
|
- **Use Storage Providers** for reusable connections across multiple transfers
|
||||||
|
- **Update credentials in one place** by using Storage Providers instead of updating each transfer individually
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
title: Monitoring
|
||||||
|
---
|
||||||
|
|
||||||
|
# Monitoring and Reporting
|
||||||
|
|
||||||
|
GoMFT provides comprehensive monitoring and reporting features to help you track transfer activities, analyze performance, and ensure reliable operation. This page explains the monitoring tools available in GoMFT.
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
The GoMFT dashboard provides a real-time overview of your file transfer system:
|
||||||
|
|
||||||
|
### Dashboard Components
|
||||||
|
|
||||||
|
- **Transfer Status**: Overview of currently running, completed, and failed transfers
|
||||||
|
- **Recent Transfers**: List of the most recent transfer executions
|
||||||
|
- **System Health**: Indicators for system health and resource usage
|
||||||
|
- **Quick Actions**: Buttons for common tasks like creating transfers or checking logs
|
||||||
|
|
||||||
|
To access the dashboard:
|
||||||
|
1. Log in to GoMFT
|
||||||
|
2. The dashboard is the default landing page
|
||||||
|
3. You can return to it anytime by clicking **Dashboard** in the sidebar
|
||||||
|
|
||||||
|
## Transfer History
|
||||||
|
|
||||||
|
The transfer history section provides detailed information about all transfer executions:
|
||||||
|
|
||||||
|
### Transfer History Features
|
||||||
|
|
||||||
|
- **Comprehensive Logs**: Complete transfer history with filtering options
|
||||||
|
- **Status Tracking**: Visual indicators for transfer status (successful, failed, in progress)
|
||||||
|
- **Performance Metrics**: Data on transfer speed, file counts, and total bytes
|
||||||
|
- **Time Tracking**: Start time, end time, and duration for all transfers
|
||||||
|
- **Filter and Search**: Find specific transfers by name, status, date, or other criteria
|
||||||
|
|
||||||
|
To access transfer history:
|
||||||
|
1. Navigate to **Transfer History** in the sidebar
|
||||||
|
2. Use filters to narrow down the list of transfers
|
||||||
|
3. Click on any transfer to see detailed information
|
||||||
|
|
||||||
|
## Real-Time Monitoring
|
||||||
|
|
||||||
|
GoMFT provides real-time monitoring of active transfers:
|
||||||
|
|
||||||
|
### Active Transfers
|
||||||
|
|
||||||
|
- **Live Progress**: See transfer progress as it happens
|
||||||
|
- **File Counters**: Track files transferred, remaining, and skipped
|
||||||
|
- **Bandwidth Usage**: Monitor current transfer speeds
|
||||||
|
- **Cancel Option**: Ability to cancel running transfers
|
||||||
|
- **Log Streaming**: View logs as they're generated
|
||||||
|
|
||||||
|
To monitor active transfers:
|
||||||
|
1. Navigate to **Transfer History** in the sidebar
|
||||||
|
2. View all currently running transfers
|
||||||
|
3. Click on any transfer to see detailed progress
|
||||||
|
|
||||||
|
## Detailed Transfer Logs
|
||||||
|
|
||||||
|
For each transfer execution, GoMFT maintains detailed logs:
|
||||||
|
|
||||||
|
### Log Information
|
||||||
|
|
||||||
|
- **File Details**: Information about each transferred file
|
||||||
|
- **Error Messages**: Detailed error information for failed transfers
|
||||||
|
- **Warning Messages**: Warnings that occurred during transfer
|
||||||
|
- **Transfer Summary**: Overall summary of the transfer operation
|
||||||
|
- **Performance Data**: Transfer rates and timing information
|
||||||
|
|
||||||
|
To access detailed logs:
|
||||||
|
1. Navigate to **Transfer History** in the sidebar
|
||||||
|
2. Find the transfer of interest
|
||||||
|
3. Click on **View Details** to open details
|
||||||
|
|
||||||
|
## System Monitoring
|
||||||
|
|
||||||
|
GoMFT monitors the health and performance of the system itself:
|
||||||
|
|
||||||
|
## Alerts and Notifications
|
||||||
|
|
||||||
|
GoMFT can alert you to important events:
|
||||||
|
|
||||||
|
### Alert Types
|
||||||
|
|
||||||
|
- **Transfer Failures**: Notifications when transfers fail
|
||||||
|
- **Transfer Completion**: Alerts when transfers complete
|
||||||
|
|
||||||
|
To configure alerts:
|
||||||
|
1. Navigate to **Notification Providers**
|
||||||
|
2. Set up notification methods (email, webhook, gotify, ntfy, pushover, pushbullet)
|
||||||
|
4. Configure alert severity levels
|
||||||
|
|
||||||
|
## Export and API Access
|
||||||
|
|
||||||
|
GoMFT allows you to export monitoring data:
|
||||||
|
|
||||||
|
### Export Options
|
||||||
|
|
||||||
|
- **CSV Export**: Download transfer history in CSV format
|
||||||
|
- **JSON Export**: Export data in JSON format for further processing
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Review the dashboard daily** to stay informed of transfer status
|
||||||
|
- **Set up alerts** for critical transfers to be notified of failures
|
||||||
|
- **Generate regular reports** for compliance and performance tracking
|
||||||
|
- **Monitor system health** to prevent resource issues
|
||||||
|
- **Archive logs** for long-term storage and compliance
|
||||||
|
- **Use filters** to focus on the most important information
|
||||||
|
- **Export data** for backup and external analysis
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Schedules
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schedule Management
|
||||||
|
|
||||||
|
GoMFT's scheduling system allows you to automate file transfers to run at specific times or on a recurring basis. This section explains how to create, manage, and monitor scheduled transfers.
|
||||||
|
|
||||||
|
## Schedule Types
|
||||||
|
|
||||||
|
GoMFT supports several types of schedules:
|
||||||
|
|
||||||
|
### One-Time Schedules
|
||||||
|
|
||||||
|
Run a transfer once at a specific date and time.
|
||||||
|
|
||||||
|
### Recurring Schedules
|
||||||
|
|
||||||
|
Run a transfer repeatedly according to a defined pattern:
|
||||||
|
|
||||||
|
- **Hourly**: Run every hour at a specific minute
|
||||||
|
- **Daily**: Run every day at a specific time
|
||||||
|
- **Weekly**: Run on specific days of the week
|
||||||
|
- **Monthly**: Run on specific days of the month
|
||||||
|
- **Custom**: Define a custom schedule using cron syntax
|
||||||
|
|
||||||
|
## Creating a Schedule
|
||||||
|
|
||||||
|
To create a new schedule:
|
||||||
|
|
||||||
|
1. Navigate to the **Schedules** section in the sidebar
|
||||||
|
2. Click **Create New Schedule**
|
||||||
|
3. Select the transfer to schedule
|
||||||
|
4. Choose the schedule type
|
||||||
|
5. Configure the schedule details
|
||||||
|
6. Set additional options
|
||||||
|
7. Click **Save Schedule**
|
||||||
|
|
||||||
|
## Schedule Configuration
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
- **Name**: A descriptive name for the schedule
|
||||||
|
- **Transfer**: The transfer configuration to run
|
||||||
|
- **Enabled**: Toggle to enable or disable the schedule
|
||||||
|
- **Schedule Type**: One-time or recurring
|
||||||
|
|
||||||
|
### One-Time Schedule Options
|
||||||
|
|
||||||
|
- **Date**: The date to run the transfer
|
||||||
|
- **Time**: The time to run the transfer
|
||||||
|
|
||||||
|
### Recurring Schedule Options
|
||||||
|
|
||||||
|
#### Simple Options
|
||||||
|
|
||||||
|
- **Frequency**: Hourly, Daily, Weekly, Monthly, or Custom
|
||||||
|
- **Time**: The time to run (for Daily, Weekly, Monthly)
|
||||||
|
- **Days**: The days to run (for Weekly, Monthly)
|
||||||
|
- **Minutes**: The minute to run (for Hourly)
|
||||||
|
|
||||||
|
#### Advanced Options (Cron Syntax)
|
||||||
|
|
||||||
|
For more complex scheduling needs, you can use cron syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────── minute (0 - 59)
|
||||||
|
│ ┌──────── hour (0 - 23)
|
||||||
|
│ │ ┌────── day of month (1 - 31)
|
||||||
|
│ │ │ ┌──── month (1 - 12)
|
||||||
|
│ │ │ │ ┌── day of week (0 - 6) (Sunday to Saturday)
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
* * * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `0 2 * * *`: Every day at 2:00 AM
|
||||||
|
- `0 9-17 * * 1-5`: Every hour from 9 AM to 5 PM, Monday to Friday
|
||||||
|
- `*/15 * * * *`: Every 15 minutes
|
||||||
|
- `0 0 1,15 * *`: 1st and 15th of every month at midnight
|
||||||
|
|
||||||
|
### Additional Options
|
||||||
|
|
||||||
|
- **Timeout**: Maximum duration for the transfer (after which it will be terminated)
|
||||||
|
- **Retry Count**: Number of times to retry on failure
|
||||||
|
- **Retry Delay**: Time to wait between retry attempts
|
||||||
|
- **Priority**: Schedule priority (higher priority schedules run first when multiple are due)
|
||||||
|
- **Description**: Additional notes about the schedule
|
||||||
|
|
||||||
|
## Schedule Groups
|
||||||
|
|
||||||
|
GoMFT allows you to organize schedules into logical groups:
|
||||||
|
|
||||||
|
1. Navigate to **Schedule Groups** in the Schedules section
|
||||||
|
2. Create a new group with a name and description
|
||||||
|
3. Assign schedules to the group
|
||||||
|
4. View and manage grouped schedules together
|
||||||
|
|
||||||
|
Benefits of groups:
|
||||||
|
- Organize related schedules
|
||||||
|
- Apply batch operations to multiple schedules
|
||||||
|
- Monitor group-level statistics
|
||||||
|
|
||||||
|
## Managing Schedules
|
||||||
|
|
||||||
|
### Viewing Schedules
|
||||||
|
|
||||||
|
The **Schedules** page displays all configured schedules with:
|
||||||
|
- Schedule name
|
||||||
|
- Associated transfer
|
||||||
|
- Next run time
|
||||||
|
- Last run status
|
||||||
|
- Enabled/disabled status
|
||||||
|
|
||||||
|
### Editing Scheduled Jobs
|
||||||
|
|
||||||
|
To edit an existing schedule:
|
||||||
|
1. Navigate to the **Scheduled Jobs** section
|
||||||
|
2. Find the schedule you want to edit
|
||||||
|
3. Click the **Edit** button
|
||||||
|
4. Modify the schedule details
|
||||||
|
5. Save your changes
|
||||||
|
|
||||||
|
### Enabling/Disabling Scheduled Jobs
|
||||||
|
|
||||||
|
To temporarily disable a schedule without deleting it:
|
||||||
|
1. Navigate to the **Scheduled Job** section
|
||||||
|
2. Find the schedule you want to disable
|
||||||
|
3. Click edit
|
||||||
|
3. Toggle the **Enabled** switch to Off and save
|
||||||
|
4. The schedule will remain configured but won't run until re-enabled
|
||||||
|
|
||||||
|
### Deleting Schedules
|
||||||
|
|
||||||
|
To delete a schedule:
|
||||||
|
1. Navigate to the **Scheduled Job** section
|
||||||
|
2. Find the schedule you want to delete
|
||||||
|
3. Click the **Delete** button
|
||||||
|
4. Confirm the deletion
|
||||||
|
|
||||||
|
## Schedule Execution
|
||||||
|
|
||||||
|
When a schedule runs, GoMFT performs these actions:
|
||||||
|
|
||||||
|
1. Identifies schedules due for execution
|
||||||
|
2. Prioritizes schedules based on priority setting
|
||||||
|
3. Creates execution jobs for the associated transfers
|
||||||
|
4. Monitors job execution
|
||||||
|
5. Records results in the history
|
||||||
|
6. Handles retries if configured and needed
|
||||||
|
7. Updates next run time for recurring schedules
|
||||||
|
|
||||||
|
## Monitoring Schedules
|
||||||
|
|
||||||
|
GoMFT provides several ways to monitor your scheduled transfers:
|
||||||
|
|
||||||
|
### Transfer Calendar
|
||||||
|
|
||||||
|
The Transfer Calendar provides a visual overview of all your scheduled transfers:
|
||||||
|
|
||||||
|
1. Navigate to **Transfer Calendar** in the sidebar
|
||||||
|
2. View all scheduled transfers in a monthly, weekly, or daily view
|
||||||
|
3. Color-coded events indicate different transfer types or statuses
|
||||||
|
4. Hover over any event to see a summary of the transfer details
|
||||||
|
5. Click on any scheduled transfer to see full details or edit it
|
||||||
|
|
||||||
|
#### Calendar Views
|
||||||
|
|
||||||
|
- **Month View**: See all scheduled transfers for the entire month
|
||||||
|
- **Week View**: Focus on transfers scheduled for the current week
|
||||||
|
- **Day View**: Detailed timeline of transfers for a specific day
|
||||||
|
- **Agenda View**: List-based view of upcoming transfers
|
||||||
|
|
||||||
|
#### Calendar Features
|
||||||
|
|
||||||
|
- **Filtering**: Filter transfers by type, status, or associated connection
|
||||||
|
<!-- - **Search**: Find specific transfers by name or description -->
|
||||||
|
<!-- - **Export**: Export calendar events to iCal or CSV format -->
|
||||||
|
<!-- - **Drag and Drop**: Reschedule transfers by dragging them to a new time slot (requires appropriate permissions) -->
|
||||||
|
<!-- - **Conflict Detection**: Visual indicators for potentially overlapping transfers -->
|
||||||
|
|
||||||
|
<!-- #### Calendar Integration
|
||||||
|
|
||||||
|
You can subscribe to the transfer calendar using external calendar applications:
|
||||||
|
|
||||||
|
1. Click the **Calendar Subscription** button
|
||||||
|
2. Copy the provided iCal URL
|
||||||
|
3. Add the URL as a calendar subscription in applications like Google Calendar, Outlook, or Apple Calendar
|
||||||
|
4. Set the refresh frequency in your calendar application
|
||||||
|
|
||||||
|
> **Note**: The calendar subscription is read-only and requires authentication. Calendar subscriptions will only show transfers that the authenticated user has permission to view. -->
|
||||||
|
|
||||||
|
### Transfer History
|
||||||
|
|
||||||
|
View the execution history of your schedules:
|
||||||
|
1. Navigate to **Transfer History** on the sidebar
|
||||||
|
2. See when schedules ran, their status, and execution details
|
||||||
|
3. Filter by date range, status, or schedule name
|
||||||
|
|
||||||
|
## Schedule Notifications
|
||||||
|
|
||||||
|
Configure notifications for scheduled transfers:
|
||||||
|
|
||||||
|
1. Edit a schedule
|
||||||
|
2. Navigate to the **Notifications** tab
|
||||||
|
3. Configure email notifications or webhooks
|
||||||
|
4. Specify notification conditions (success, failure, or both)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use descriptive names** for schedules to easily identify them
|
||||||
|
- **Set appropriate timeouts** based on expected transfer duration
|
||||||
|
- **Configure retries** for critical transfers
|
||||||
|
- **Use schedule groups** to organize related schedules
|
||||||
|
- **Stagger schedules** to avoid resource contention
|
||||||
|
- **Set up notifications** for critical schedules
|
||||||
|
- **Review schedule history** regularly to identify issues
|
||||||
|
- **Disable schedules** instead of deleting them for temporary pauses
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Transfers
|
||||||
|
---
|
||||||
|
|
||||||
|
# Transfer Operations
|
||||||
|
|
||||||
|
GoMFT's primary function is to manage file transfers between different storage systems. This page explains the transfer operations available in GoMFT and how to configure them.
|
||||||
|
|
||||||
|
> **Note**: GoMFT now supports the Storage Provider feature, which allows you to create reusable connection profiles for your transfers. For detailed information, see the [Storage Providers](/docs/user-guides/storage-provider-guide) guide.
|
||||||
|
|
||||||
|
## Transfer Types
|
||||||
|
|
||||||
|
GoMFT supports several types of transfer operations, each with different behaviors:
|
||||||
|
|
||||||
|
### Copy
|
||||||
|
|
||||||
|
The **Copy** operation copies files from the source to the destination. Files are only copied if they don't exist at the destination or if they've been modified at the source.
|
||||||
|
|
||||||
|
```
|
||||||
|
Source → Destination
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sync
|
||||||
|
|
||||||
|
The **Sync** operation makes the destination identical to the source, adding, removing, and updating files as necessary.
|
||||||
|
|
||||||
|
```
|
||||||
|
Source → Destination (with deletions)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move
|
||||||
|
|
||||||
|
The **Move** operation copies files from the source to the destination and then deletes the source files after a successful transfer.
|
||||||
|
|
||||||
|
```
|
||||||
|
Source → Destination → Delete Source
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bidirectional Sync
|
||||||
|
|
||||||
|
The **Bidirectional Sync** operation synchronizes files in both directions, ensuring that the newest version of each file is present in both locations.
|
||||||
|
|
||||||
|
```
|
||||||
|
Source ⟷ Destination
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transfer Configuration
|
||||||
|
|
||||||
|
When creating a transfer in GoMFT, you need to configure the following elements:
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
- **Name**: A descriptive name for the transfer
|
||||||
|
- **Description**: Optional details about the transfer's purpose
|
||||||
|
- **Source**: Either a direct connection configuration or a Storage Provider
|
||||||
|
- **Destination**: Either a direct connection configuration or a Storage Provider
|
||||||
|
- **Transfer Type**: Copy, Sync, Move, or Bidirectional Sync
|
||||||
|
|
||||||
|
#### Using Storage Providers
|
||||||
|
|
||||||
|
When creating a transfer, you can now select a Storage Provider instead of entering connection details directly:
|
||||||
|
|
||||||
|
1. In the Source or Destination section, select **Provider** from the dropdown
|
||||||
|
2. Choose from your available Storage Providers
|
||||||
|
3. Enter the path within the selected provider
|
||||||
|
|
||||||
|
This approach offers several benefits:
|
||||||
|
- Reuse the same provider across multiple transfers
|
||||||
|
- Update credentials in one place
|
||||||
|
- Enhanced security with AES-256 encryption for credentials
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
#### File Selection
|
||||||
|
|
||||||
|
- **Include Patterns**: Patterns for files to include (e.g., `*.txt`, `data/**/*.csv`)
|
||||||
|
- **Exclude Patterns**: Patterns for files to exclude (e.g., `*.tmp`, `**/._*`)
|
||||||
|
- **Min Size**: Minimum file size to transfer
|
||||||
|
- **Max Size**: Maximum file size to transfer
|
||||||
|
- **Min Age**: Only transfer files older than this
|
||||||
|
- **Max Age**: Only transfer files newer than this
|
||||||
|
|
||||||
|
#### Transfer Behavior
|
||||||
|
|
||||||
|
- **Checksum**: Compare files using checksums instead of size/date
|
||||||
|
- **Delete Before**: Delete destination files before transferring
|
||||||
|
- **Delete During**: Delete destination files during transfer
|
||||||
|
- **Delete After**: Delete destination files not in source after transfer
|
||||||
|
- **Update Existing**: Update existing files at destination
|
||||||
|
- **Skip New**: Skip new files not present at destination
|
||||||
|
- **Skip Newer**: Skip files that are newer at the destination
|
||||||
|
|
||||||
|
#### Performance Options
|
||||||
|
|
||||||
|
- **Transfers**: Number of concurrent file transfers
|
||||||
|
- **Checkers**: Number of concurrent file checkers
|
||||||
|
- **Bandwidth Limit**: Maximum bandwidth to use in bytes/s
|
||||||
|
- **Buffer Size**: Size of transfer buffer (default: 16MB)
|
||||||
|
- **Chunk Size**: Upload chunk size for chunked uploads
|
||||||
|
|
||||||
|
## Transfer Execution
|
||||||
|
|
||||||
|
### Manual Execution
|
||||||
|
|
||||||
|
Transfers can be run on-demand:
|
||||||
|
|
||||||
|
1. Navigate to the **Scheduled Jobs** section
|
||||||
|
2. Find your job in the list
|
||||||
|
3. Click **Run Now**
|
||||||
|
4. Monitor the job progress in real-time
|
||||||
|
|
||||||
|
### Scheduled Execution
|
||||||
|
|
||||||
|
Transfers can be scheduled to run automatically:
|
||||||
|
|
||||||
|
1. Navigate to the **Scheduled Jobs** section
|
||||||
|
2. Create a new schedule linked to your transfer configuration
|
||||||
|
3. Set up the schedule using cron syntax or the schedule builder
|
||||||
|
4. The transfer will run automatically according to the schedule
|
||||||
|
|
||||||
|
## Transfer Monitoring
|
||||||
|
|
||||||
|
### Status Indicators
|
||||||
|
|
||||||
|
- **Pending**: Transfer is waiting to start
|
||||||
|
- **Running**: Transfer is in progress
|
||||||
|
- **Completed**: Transfer finished successfully
|
||||||
|
- **Failed**: Transfer encountered an error
|
||||||
|
- **Canceled**: Transfer was manually canceled
|
||||||
|
|
||||||
|
### Transfer Details
|
||||||
|
|
||||||
|
For each transfer execution, GoMFT records:
|
||||||
|
|
||||||
|
- Start and end times
|
||||||
|
- Duration
|
||||||
|
- Number of files transferred
|
||||||
|
- Total bytes transferred
|
||||||
|
- Files skipped
|
||||||
|
- Errors encountered
|
||||||
|
- Detailed logs
|
||||||
|
|
||||||
|
## Transfer Logs
|
||||||
|
|
||||||
|
GoMFT provides detailed logs for each transfer:
|
||||||
|
|
||||||
|
1. Navigate to **Transfer History**
|
||||||
|
2. Click on a **View Details** button on the transfer entry
|
||||||
|
|
||||||
|
The logs include information about:
|
||||||
|
|
||||||
|
- Each file transferred
|
||||||
|
- Skipped files
|
||||||
|
- Errors
|
||||||
|
- Performance metrics
|
||||||
|
- Overall transfer summary
|
||||||
|
|
||||||
|
## Troubleshooting Failed Transfers
|
||||||
|
|
||||||
|
When a transfer fails, GoMFT provides information to help identify the cause:
|
||||||
|
|
||||||
|
1. Check the error message in the transfer history
|
||||||
|
2. Review the detailed logs for the specific error
|
||||||
|
3. For transfers using Storage Providers, you can test the provider connection directly from the Storage Providers section
|
||||||
|
4. Common issues include:
|
||||||
|
- Permission problems
|
||||||
|
- Network connectivity
|
||||||
|
- Invalid credentials
|
||||||
|
- Path not found
|
||||||
|
- Disk space issues
|
||||||
|
- Expired tokens (for OAuth providers like OneDrive or Google Drive)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use meaningful names** for your transfers to easily identify them
|
||||||
|
- **Start small** when testing new configurations
|
||||||
|
- **Use include/exclude patterns** to limit scope when working with large directories
|
||||||
|
- **Set appropriate concurrency** based on network conditions and system resources
|
||||||
|
- **Use checksumming** for critical data to ensure integrity
|
||||||
|
- **Set bandwidth limits** to avoid network congestion during peak hours
|
||||||
|
- **Schedule large transfers** during off-peak times
|
||||||
|
- **Use notifications** to stay informed about transfer results
|
||||||
|
- **Regularly review logs** to identify potential issues
|
||||||
|
- **Use Storage Providers** for reusable connections across multiple transfers
|
||||||
|
- **Convert existing transfers** to use Storage Providers for easier credential management
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
title: Code Review Guidelines
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review Guidelines
|
||||||
|
|
||||||
|
This document outlines the code review process and expectations for the GoMFT project.
|
||||||
|
|
||||||
|
## Purpose of Code Reviews
|
||||||
|
|
||||||
|
Code reviews serve several important purposes:
|
||||||
|
|
||||||
|
- Ensuring code quality and consistency
|
||||||
|
- Identifying bugs, edge cases, and potential issues early
|
||||||
|
- Sharing knowledge among team members
|
||||||
|
- Ensuring adherence to project standards and best practices
|
||||||
|
- Validating that the implementation meets requirements
|
||||||
|
|
||||||
|
## Code Review Process
|
||||||
|
|
||||||
|
### 1. Before Requesting a Review
|
||||||
|
|
||||||
|
Before requesting a review, ensure your code:
|
||||||
|
|
||||||
|
- Passes all automated tests
|
||||||
|
- Follows the project's coding standards
|
||||||
|
- Is well-documented
|
||||||
|
- Includes appropriate tests
|
||||||
|
- Has a clear, descriptive PR title and description
|
||||||
|
|
||||||
|
### 2. Requesting a Review
|
||||||
|
|
||||||
|
- Create a pull request against the appropriate branch
|
||||||
|
- Fill out the PR template completely
|
||||||
|
- Tag appropriate reviewers based on the code being changed
|
||||||
|
- Respond to any automated CI/CD feedback
|
||||||
|
|
||||||
|
### 3. Conducting a Review
|
||||||
|
|
||||||
|
When reviewing code, focus on:
|
||||||
|
|
||||||
|
#### Code Quality
|
||||||
|
- Is the code readable and maintainable?
|
||||||
|
- Does it follow project conventions and patterns?
|
||||||
|
- Is the implementation efficient?
|
||||||
|
- Are edge cases handled appropriately?
|
||||||
|
|
||||||
|
#### Functionality
|
||||||
|
- Does the code meet the requirements?
|
||||||
|
- Does it handle error conditions properly?
|
||||||
|
- Is the user experience considered?
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
- Are there sufficient tests?
|
||||||
|
- Do tests cover edge cases?
|
||||||
|
- Are tests reliable (not flaky)?
|
||||||
|
|
||||||
|
#### Security
|
||||||
|
- Are there potential security issues?
|
||||||
|
- Is user input properly validated?
|
||||||
|
- Are credentials or sensitive data handled securely?
|
||||||
|
|
||||||
|
#### Documentation
|
||||||
|
- Is the code well-documented?
|
||||||
|
- Are public APIs clearly documented?
|
||||||
|
- Is the documentation accurate and up-to-date?
|
||||||
|
|
||||||
|
### 4. Providing Feedback
|
||||||
|
|
||||||
|
When providing feedback:
|
||||||
|
|
||||||
|
- Be specific and clear
|
||||||
|
- Offer suggestions for improvement
|
||||||
|
- Differentiate between required changes and optional suggestions
|
||||||
|
- Provide context or reasoning for requested changes
|
||||||
|
- Be constructive and respectful
|
||||||
|
|
||||||
|
Use these comment prefixes to indicate the severity of feedback:
|
||||||
|
|
||||||
|
- **Blocker:** Must be addressed before merging
|
||||||
|
- **Suggestion:** Recommended improvement, but not required
|
||||||
|
- **Question:** Request for clarification
|
||||||
|
- **Nitpick:** Minor style or formatting issue
|
||||||
|
- **Praise:** Highlight particularly good code
|
||||||
|
|
||||||
|
### 5. Responding to Feedback
|
||||||
|
|
||||||
|
When receiving review feedback:
|
||||||
|
|
||||||
|
- Address all comments
|
||||||
|
- Explain your reasoning if you disagree with a suggestion
|
||||||
|
- Ask for clarification if needed
|
||||||
|
- Thank reviewers for their input
|
||||||
|
- Mark resolved comments as such
|
||||||
|
|
||||||
|
### 6. Approving and Merging
|
||||||
|
|
||||||
|
A PR can be merged when:
|
||||||
|
|
||||||
|
- It has received approval from at least one reviewer
|
||||||
|
- All "Blocker" issues are resolved
|
||||||
|
- All automated checks are passing
|
||||||
|
- The PR has been rebased on the latest target branch
|
||||||
|
|
||||||
|
## Best Practices for Reviewers
|
||||||
|
|
||||||
|
### Focus on the Important Things
|
||||||
|
- Prioritize correctness, security, and maintainability
|
||||||
|
- Don't get too caught up in style issues that could be automated
|
||||||
|
- Consider the big picture and overall architecture
|
||||||
|
|
||||||
|
### Be Timely
|
||||||
|
- Try to review PRs within 1-2 business days
|
||||||
|
- If you can't review promptly, let the author know or reassign
|
||||||
|
- For urgent fixes, prioritize those reviews
|
||||||
|
|
||||||
|
### Be Thorough
|
||||||
|
- Take the time to understand the code
|
||||||
|
- Test the code locally if necessary
|
||||||
|
- Consider edge cases and failure modes
|
||||||
|
|
||||||
|
### Be Respectful
|
||||||
|
- Focus on the code, not the person
|
||||||
|
- Phrase feedback as suggestions or questions
|
||||||
|
- Acknowledge good work and improvements
|
||||||
|
|
||||||
|
## Best Practices for Authors
|
||||||
|
|
||||||
|
### Keep PRs Focused
|
||||||
|
- Each PR should address a single concern
|
||||||
|
- Large changes should be broken into smaller, logical PRs
|
||||||
|
- Avoid unrelated changes in a PR
|
||||||
|
|
||||||
|
### Provide Context
|
||||||
|
- Explain the purpose and approach in the PR description
|
||||||
|
- Link to relevant issues or documentation
|
||||||
|
- Point out areas where you're uncertain or would like specific feedback
|
||||||
|
|
||||||
|
### Respond Promptly
|
||||||
|
- Address feedback in a timely manner
|
||||||
|
- Ask questions if feedback is unclear
|
||||||
|
- Be open to suggestions and alternatives
|
||||||
|
|
||||||
|
### Test Thoroughly
|
||||||
|
- Test your changes locally before requesting review
|
||||||
|
- Consider edge cases and error scenarios
|
||||||
|
- Update tests to cover new functionality
|
||||||
|
|
||||||
|
## Special Considerations
|
||||||
|
|
||||||
|
### Security-Related Changes
|
||||||
|
- Security-focused changes require extra scrutiny
|
||||||
|
- At least one reviewer should have security expertise
|
||||||
|
- Consider potential attack vectors and edge cases
|
||||||
|
|
||||||
|
### API Changes
|
||||||
|
- Changes to public APIs require careful review
|
||||||
|
- Consider backward compatibility
|
||||||
|
- Ensure API changes are well-documented
|
||||||
|
|
||||||
|
### Database Changes
|
||||||
|
- Review for potential performance issues
|
||||||
|
- Consider migration strategy and backward compatibility
|
||||||
|
- Validate data integrity considerations
|
||||||
|
|
||||||
|
### UI Changes
|
||||||
|
- Consider accessibility implications
|
||||||
|
- Review for consistency with design standards
|
||||||
|
- Test on different devices and screen sizes
|
||||||
|
|
||||||
|
## Learning from Code Reviews
|
||||||
|
|
||||||
|
Code reviews are a learning opportunity:
|
||||||
|
|
||||||
|
- Take note of recurring feedback to improve future code
|
||||||
|
- Share knowledge gained from reviews with the team
|
||||||
|
- Use reviews to identify areas where documentation or guides could be improved
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
### General
|
||||||
|
- [ ] Code is well-structured and follows project patterns
|
||||||
|
- [ ] Variables and functions have clear, descriptive names
|
||||||
|
- [ ] Comments explain "why" not just "what"
|
||||||
|
- [ ] Unnecessary code is removed (commented code, debug logs)
|
||||||
|
- [ ] No hardcoded values that should be configurable
|
||||||
|
|
||||||
|
### Go-Specific
|
||||||
|
- [ ] Error handling is appropriate and consistent
|
||||||
|
- [ ] Follows Go idioms and best practices
|
||||||
|
- [ ] Concurrent code is safe and efficient
|
||||||
|
- [ ] Uses appropriate Go standard library functions
|
||||||
|
- [ ] Properly handles resources (file handles, connections)
|
||||||
|
|
||||||
|
### Frontend-Specific
|
||||||
|
- [ ] UI is responsive and accessible
|
||||||
|
- [ ] HTMX usage follows project patterns
|
||||||
|
- [ ] Templ templates are clean and maintainable
|
||||||
|
- [ ] JavaScript is minimal and follows best practices
|
||||||
|
- [ ] CSS follows project conventions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Tests are included for new functionality
|
||||||
|
- [ ] Tests cover edge cases and error paths
|
||||||
|
- [ ] Tests are clear and maintainable
|
||||||
|
- [ ] Mocks and fixtures are used appropriately
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [ ] Input validation is thorough
|
||||||
|
- [ ] No SQL injection vulnerabilities
|
||||||
|
- [ ] Authentication and authorization are properly implemented
|
||||||
|
- [ ] Sensitive data is handled securely
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- [ ] Code is efficient for expected scale
|
||||||
|
- [ ] Database queries are optimized
|
||||||
|
- [ ] Appropriate caching is used
|
||||||
|
- [ ] Resources are used efficiently
|
||||||
|
|
||||||
|
Remember that code reviews are a collaborative process aimed at improving the overall quality of the codebase. Both reviewers and authors should approach the process with a growth mindset and mutual respect.
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Contributing
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contributing to GoMFT
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to GoMFT! This guide will help you get started with contributing to the project.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
By participating in this project, you agree to abide by our Code of Conduct. Please read it before contributing.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have the following installed:
|
||||||
|
|
||||||
|
- **Go** (version 1.20 or later)
|
||||||
|
- **Node.js** (version 18 or later)
|
||||||
|
- **Git**
|
||||||
|
- **Docker** (optional, for container-based development)
|
||||||
|
|
||||||
|
### Setting Up the Development Environment
|
||||||
|
|
||||||
|
1. Fork the repository on GitHub.
|
||||||
|
|
||||||
|
2. Clone your forked repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/GoMFT.git
|
||||||
|
cd GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add the original repository as an upstream remote:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/StarFleetCPTN/GoMFT.git
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install Go dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Install Node.js dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install the Templ compiler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Install Air for live reloading during development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/cosmtrek/air@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. Create a new branch for your feature or bug fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make your changes to the codebase.
|
||||||
|
|
||||||
|
3. Compile the Templ templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the development server with Air:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
air
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the application with hot reloading enabled, so changes to Go files will trigger a rebuild.
|
||||||
|
|
||||||
|
5. For frontend development, compile the Tailwind CSS and watch for changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Access the application at `http://localhost:8080`.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
See the [Project Structure](/docs/development/project-structure) page for a detailed overview of the codebase organization.
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
|
||||||
|
### Go Code
|
||||||
|
|
||||||
|
- Follow the [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) and [Effective Go](https://golang.org/doc/effective_go) guidelines.
|
||||||
|
- Format your code with `gofmt` or `go fmt`.
|
||||||
|
- Ensure your code passes `golint` and `go vet`.
|
||||||
|
- Write tests for your functionality.
|
||||||
|
- Add comments to exported functions, types, and packages.
|
||||||
|
|
||||||
|
### Frontend Code
|
||||||
|
|
||||||
|
- Follow the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for JavaScript code.
|
||||||
|
- Use Tailwind CSS for styling.
|
||||||
|
- Ensure your UI components are responsive.
|
||||||
|
- Test your UI changes in different browsers.
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
- Use clear and meaningful commit messages.
|
||||||
|
- Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||||
|
- `feat`: A new feature
|
||||||
|
- `fix`: A bug fix
|
||||||
|
- `docs`: Documentation only changes
|
||||||
|
- `style`: Changes that do not affect the meaning of the code
|
||||||
|
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
||||||
|
- `test`: Adding missing tests or correcting existing tests
|
||||||
|
- `chore`: Changes to the build process or auxiliary tools
|
||||||
|
- `perf`: Performance improvements
|
||||||
|
|
||||||
|
Example: `feat: add email notification for failed transfers`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Run the Go tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run specific tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/api/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
- Write unit tests for your functions and methods.
|
||||||
|
- Write integration tests for API endpoints.
|
||||||
|
- Aim for high test coverage, especially for critical functionality.
|
||||||
|
- Use table-driven tests where appropriate.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Update your branch with the latest changes from upstream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch upstream
|
||||||
|
git rebase upstream/main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Push your branch to your forked repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a pull request from your branch to the main repository.
|
||||||
|
|
||||||
|
4. Ensure your PR description clearly describes the changes you've made.
|
||||||
|
|
||||||
|
5. Link any relevant issues in your PR description.
|
||||||
|
|
||||||
|
6. Wait for code review and address any feedback.
|
||||||
|
|
||||||
|
### PR Review Checklist
|
||||||
|
|
||||||
|
Before submitting your PR, please ensure:
|
||||||
|
|
||||||
|
- [ ] Your code builds without errors or warnings
|
||||||
|
- [ ] You've added tests for your changes
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Your code follows the project's coding guidelines
|
||||||
|
- [ ] You've updated documentation as needed
|
||||||
|
- [ ] You've added appropriate logging
|
||||||
|
- [ ] You've considered security implications
|
||||||
|
- [ ] Your changes don't introduce performance regressions
|
||||||
|
|
||||||
|
## Development Tips
|
||||||
|
|
||||||
|
### Working with Templ
|
||||||
|
|
||||||
|
[Templ](https://github.com/a-h/templ) is used for HTML templating in GoMFT. After making changes to `.templ` files, you need to regenerate the Go code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with HTMX
|
||||||
|
|
||||||
|
[HTMX](https://htmx.org/) is used for dynamic UI updates. Familiarize yourself with its concepts before making UI changes.
|
||||||
|
|
||||||
|
### Working with SQLite
|
||||||
|
|
||||||
|
GoMFT uses SQLite for data storage. The database file is located at `data/gomft.db` by default. You can use a tool like [SQLite Browser](https://sqlitebrowser.org/) to inspect the database.
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
For debugging Go code, you can use:
|
||||||
|
|
||||||
|
- `fmt.Printf()` statements for simple debugging
|
||||||
|
- [Delve](https://github.com/go-delve/delve) for more complex debugging scenarios
|
||||||
|
- VSCode's integrated Go debugger
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Updating Documentation
|
||||||
|
|
||||||
|
Documentation is written in Markdown and stored in the `docs/` directory. To update the documentation:
|
||||||
|
|
||||||
|
1. Edit the relevant Markdown files.
|
||||||
|
2. If you're adding new pages, update the sidebar configuration in `sidebars.ts`.
|
||||||
|
3. Preview your changes using the documentation development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd documentation
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Access the documentation at `http://localhost:3000`.
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
API documentation is generated from Go comments using [Swaggo](https://github.com/swaggo/swag). To update the API documentation:
|
||||||
|
|
||||||
|
1. Update the API comments following the Swagger/OpenAPI format.
|
||||||
|
2. Regenerate the API documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
swag init -g cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
GoMFT follows [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
### Creating a Release
|
||||||
|
|
||||||
|
1. Update the version number in relevant files:
|
||||||
|
- `VERSION` file
|
||||||
|
- `package.json`
|
||||||
|
- `internal/version/version.go`
|
||||||
|
|
||||||
|
2. Update the CHANGELOG.md file with the new version and its changes.
|
||||||
|
|
||||||
|
3. Create a new tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v1.2.3 -m "Release v1.2.3"
|
||||||
|
git push origin v1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
4. The CI/CD pipeline will build and publish the release artifacts.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you need help with contributing to GoMFT, you can:
|
||||||
|
|
||||||
|
- Open an issue on GitHub with questions
|
||||||
|
- Discuss in the GitHub Discussions section
|
||||||
|
- Reach out to the maintainers
|
||||||
|
|
||||||
|
Thank you for contributing to GoMFT!
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Project Structure
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
This page explains the structure of the GoMFT codebase to help developers understand how the application is organized.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
GoMFT is built using a combination of Go for the backend and web technologies for the frontend. The application follows a modular architecture to maintain separation of concerns and facilitate testing and maintenance.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
Here's the high-level directory structure of the GoMFT project:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── components/ # Templ components for UI
|
||||||
|
├── internal/
|
||||||
|
│ ├── api/ # REST API handlers
|
||||||
|
│ ├── auth/ # Authentication/authorization
|
||||||
|
│ ├── config/ # Configuration management
|
||||||
|
│ ├── db/ # Database models and operations
|
||||||
|
│ ├── email/ # Email service for notifications and password resets
|
||||||
|
│ ├── scheduler/ # Job scheduling and execution
|
||||||
|
│ └── web/ # Web interface handlers
|
||||||
|
├── static/ # Static assets
|
||||||
|
│ ├── css/
|
||||||
|
│ └── js/
|
||||||
|
└── main.go # Application entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
|
||||||
|
GoMFT's backend is written in Go and structured around several key packages:
|
||||||
|
|
||||||
|
#### `main.go`
|
||||||
|
|
||||||
|
The entry point for the application. It initializes the application, loads configuration, sets up the database, and starts the web server.
|
||||||
|
|
||||||
|
#### `internal/`
|
||||||
|
|
||||||
|
Contains all internal packages that are not intended to be imported by other applications.
|
||||||
|
|
||||||
|
- **`api/`**: REST API implementation
|
||||||
|
- `handlers/`: API request handlers
|
||||||
|
- `middleware/`: API middleware (authentication, logging, etc.)
|
||||||
|
- `routes.go`: API route definitions
|
||||||
|
|
||||||
|
- **`auth/`**: Authentication and authorization
|
||||||
|
- `providers/`: Authentication providers (local, LDAP, OAuth)
|
||||||
|
- `middleware/`: Authentication middleware
|
||||||
|
- `rbac/`: Role-based access control
|
||||||
|
|
||||||
|
- **`config/`**: Configuration management
|
||||||
|
- `config.go`: Application configuration structure
|
||||||
|
- `env.go`: Environment variable loading
|
||||||
|
- `file.go`: Configuration file loading
|
||||||
|
|
||||||
|
- **`db/`**: Database layer
|
||||||
|
- `models/`: Database model definitions
|
||||||
|
- `migrations/`: Database schema migrations
|
||||||
|
- `repositories/`: Data access methods
|
||||||
|
|
||||||
|
- **`email/`**: Email functionality
|
||||||
|
- `templates/`: Email templates
|
||||||
|
- `sender.go`: Email sending service
|
||||||
|
|
||||||
|
- **`scheduler/`**: Job scheduling and execution
|
||||||
|
- `cron.go`: Cron-based job scheduler
|
||||||
|
- `executor.go`: Transfer job executor
|
||||||
|
- `queue.go`: Job queue management
|
||||||
|
|
||||||
|
- **`web/`**: Web interface
|
||||||
|
- `handlers/`: Web request handlers
|
||||||
|
- `middleware/`: Web middleware
|
||||||
|
- `routes.go`: Web route definitions
|
||||||
|
|
||||||
|
#### `components/`
|
||||||
|
|
||||||
|
Contains [templ](https://github.com/a-h/templ) components that define the UI. Templ is a Go HTML templating library that provides type-safe templates.
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── layouts/ # Page layouts
|
||||||
|
├── partials/ # Reusable UI components
|
||||||
|
├── pages/ # Page templates
|
||||||
|
│ ├── dashboard/
|
||||||
|
│ ├── transfers/
|
||||||
|
│ ├── connections/
|
||||||
|
│ ├── schedules/
|
||||||
|
│ └── admin/
|
||||||
|
└── htmx/ # HTMX-specific components
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
The frontend uses a combination of Tailwind CSS for styling and HTMX for dynamic interactions.
|
||||||
|
|
||||||
|
#### `static/`
|
||||||
|
|
||||||
|
Contains static assets for the web interface:
|
||||||
|
|
||||||
|
- **`css/`**: CSS files
|
||||||
|
- `main.css`: Main stylesheet (compiled from Tailwind)
|
||||||
|
|
||||||
|
- **`js/`**: JavaScript files
|
||||||
|
- `htmx.min.js`: HTMX library
|
||||||
|
- `alpine.min.js`: Alpine.js for lightweight interactivity
|
||||||
|
- `app.js`: Application-specific JavaScript
|
||||||
|
|
||||||
|
- **`img/`**: Images and icons
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
GoMFT uses several tools to build and bundle the application:
|
||||||
|
|
||||||
|
- **Go Build**: Compiles the Go code
|
||||||
|
- **Templ**: Compiles templ templates to Go code
|
||||||
|
- **esbuild**: Bundles JavaScript files
|
||||||
|
- **Tailwind CSS**: Compiles CSS
|
||||||
|
|
||||||
|
The build process is orchestrated by a combination of Go commands and npm scripts defined in `package.json`.
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
- **`.air.toml`**: Configuration for Air, a live reload tool for Go
|
||||||
|
- **`.env.example`**: Example environment variables configuration
|
||||||
|
- **`go.mod`**: Go module definition
|
||||||
|
- **`go.sum`**: Go module checksums
|
||||||
|
- **`package.json`**: npm package definition for frontend dependencies
|
||||||
|
- **`Dockerfile`**: Docker container definition
|
||||||
|
- **`docker-compose.yaml`**: Docker Compose configuration
|
||||||
|
|
||||||
|
## Database Structure
|
||||||
|
|
||||||
|
GoMFT uses GORM (Go Object Relational Mapper) with SQLite as the default database. The main database models include:
|
||||||
|
|
||||||
|
- **`User`**: User account information
|
||||||
|
- **`Role`**: User roles for RBAC
|
||||||
|
- **`Permission`**: Individual permissions
|
||||||
|
- **`Connection`**: File transfer connection configurations
|
||||||
|
- **`Transfer`**: Transfer definitions
|
||||||
|
- **`Schedule`**: Transfer schedules
|
||||||
|
- **`History`**: Transfer execution history
|
||||||
|
- **`Setting`**: Application settings
|
||||||
|
|
||||||
|
## API Structure
|
||||||
|
|
||||||
|
The REST API follows a RESTful design with these main endpoints:
|
||||||
|
|
||||||
|
- **`/api/auth`**: Authentication endpoints
|
||||||
|
- **`/api/users`**: User management
|
||||||
|
- **`/api/connections`**: Connection management
|
||||||
|
- **`/api/transfers`**: Transfer management
|
||||||
|
- **`/api/schedules`**: Schedule management
|
||||||
|
- **`/api/history`**: Transfer history
|
||||||
|
|
||||||
|
Each endpoint typically supports standard CRUD operations.
|
||||||
|
|
||||||
|
## Web Routes
|
||||||
|
|
||||||
|
The web interface is organized around these main routes:
|
||||||
|
|
||||||
|
- **`/`**: Dashboard
|
||||||
|
- **`/connections`**: Connection management
|
||||||
|
- **`/transfers`**: Transfer management
|
||||||
|
- **`/schedules`**: Schedule management
|
||||||
|
- **`/history`**: Transfer history
|
||||||
|
- **`/admin`**: Administrative functions
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
The authentication flow in GoMFT works like this:
|
||||||
|
|
||||||
|
1. User submits credentials via the login form or API
|
||||||
|
2. Credentials are validated against the configured authentication provider(s)
|
||||||
|
3. On success, a session is created for web users or a JWT token is issued for API users
|
||||||
|
4. The user's permissions are loaded based on their role
|
||||||
|
5. Requests are then authenticated via session cookie or JWT token
|
||||||
|
|
||||||
|
## Transfer Execution Flow
|
||||||
|
|
||||||
|
The transfer execution flow is as follows:
|
||||||
|
|
||||||
|
1. Transfer job is initiated (manually or via scheduler)
|
||||||
|
2. Job is added to the execution queue
|
||||||
|
3. Executor picks up the job and prepares the transfer
|
||||||
|
4. rclone is invoked with the appropriate parameters
|
||||||
|
5. Progress is monitored and logged
|
||||||
|
6. Results are recorded in the history
|
||||||
|
7. Notifications are sent if configured
|
||||||
|
|
||||||
|
## Testing Structure
|
||||||
|
|
||||||
|
GoMFT includes several types of tests:
|
||||||
|
|
||||||
|
- **Unit Tests**: Test individual functions and methods
|
||||||
|
- **Integration Tests**: Test interactions between components
|
||||||
|
- **API Tests**: Test API endpoints
|
||||||
|
- **End-to-End Tests**: Test complete user flows
|
||||||
|
|
||||||
|
Tests are organized alongside the code they're testing, following Go conventions.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation is provided in several formats:
|
||||||
|
|
||||||
|
- **Code Comments**: Go doc comments for packages and functions
|
||||||
|
- **API Documentation**: OpenAPI/Swagger documentation for the REST API
|
||||||
|
- **User Documentation**: User guides and tutorials (this documentation site)
|
||||||
|
- **README**: Project overview and quick start instructions
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Release Process
|
||||||
|
---
|
||||||
|
|
||||||
|
# Release Process
|
||||||
|
|
||||||
|
This document outlines the process for creating and publishing new releases of GoMFT.
|
||||||
|
|
||||||
|
## Version Numbering
|
||||||
|
|
||||||
|
GoMFT follows [Semantic Versioning](https://semver.org/) (SemVer) for version numbering:
|
||||||
|
|
||||||
|
- **Major version** (X.0.0): Incompatible API changes or significant architectural changes
|
||||||
|
- **Minor version** (0.X.0): New features added in a backward-compatible manner
|
||||||
|
- **Patch version** (0.0.X): Backward-compatible bug fixes and minor improvements
|
||||||
|
|
||||||
|
## Release Cycle
|
||||||
|
|
||||||
|
GoMFT follows a time-based release cycle:
|
||||||
|
|
||||||
|
- **Major releases**: Approximately once per year
|
||||||
|
- **Minor releases**: Every 2-3 months
|
||||||
|
- **Patch releases**: As needed for bug fixes and security updates
|
||||||
|
|
||||||
|
## Release Preparation
|
||||||
|
|
||||||
|
### 1. Feature Freeze
|
||||||
|
|
||||||
|
One week before a planned release:
|
||||||
|
|
||||||
|
- No new features are merged into the main branch
|
||||||
|
- Only bug fixes, documentation updates, and release preparation are allowed
|
||||||
|
- All tests must pass on the main branch
|
||||||
|
|
||||||
|
### 2. Update Documentation
|
||||||
|
|
||||||
|
- Ensure all new features are properly documented
|
||||||
|
- Update the changelog with all changes since the last release
|
||||||
|
- Review and update installation and upgrade instructions
|
||||||
|
|
||||||
|
### 3. Version Update
|
||||||
|
|
||||||
|
Update version numbers in:
|
||||||
|
|
||||||
|
- `VERSION` file in the project root
|
||||||
|
- `package.json` for frontend dependencies
|
||||||
|
- `internal/version/version.go` for the Go application
|
||||||
|
|
||||||
|
### 4. Create Release Branch
|
||||||
|
|
||||||
|
For minor and major releases, create a release branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b release/vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
This branch will be used for final testing and preparation.
|
||||||
|
|
||||||
|
### 5. Update Changelog
|
||||||
|
|
||||||
|
Update the `CHANGELOG.md` file with all changes since the last release:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [X.Y.Z] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New feature 1
|
||||||
|
- New feature 2
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Change 1
|
||||||
|
- Change 2
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bug fix 1
|
||||||
|
- Bug fix 2
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Security fix 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
### 1. Final Testing
|
||||||
|
|
||||||
|
Perform the following tests on the release branch:
|
||||||
|
|
||||||
|
- Run the full test suite
|
||||||
|
- Test installation from scratch
|
||||||
|
- Test upgrading from the previous version
|
||||||
|
- Test all major features manually
|
||||||
|
- Test on different platforms (Linux, macOS, Windows)
|
||||||
|
|
||||||
|
### 2. Create Release Commit
|
||||||
|
|
||||||
|
Once testing is complete, commit the version updates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add VERSION package.json internal/version/version.go CHANGELOG.md
|
||||||
|
git commit -m "Release vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Tag the Release
|
||||||
|
|
||||||
|
Create an annotated Git tag for the release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Merge to Main
|
||||||
|
|
||||||
|
If using a release branch, merge it back to main:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git merge release/vX.Y.Z
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Push the Tag
|
||||||
|
|
||||||
|
Push the tag to trigger the CI/CD release pipeline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Artifacts
|
||||||
|
|
||||||
|
The CI/CD pipeline automatically builds the following artifacts upon tagging:
|
||||||
|
|
||||||
|
1. **Docker Images**:
|
||||||
|
- `gomft/gomft:vX.Y.Z` - Specific version
|
||||||
|
- `gomft/gomft:latest` - Updated for stable releases only
|
||||||
|
|
||||||
|
2. **Binary Distributions**:
|
||||||
|
- Linux (amd64, arm64)
|
||||||
|
- macOS (amd64, arm64)
|
||||||
|
- Windows (amd64)
|
||||||
|
|
||||||
|
3. **Documentation**:
|
||||||
|
- Updated documentation website with the new version
|
||||||
|
|
||||||
|
## Post-Release Tasks
|
||||||
|
|
||||||
|
### 1. Create GitHub Release
|
||||||
|
|
||||||
|
Create a new release on GitHub:
|
||||||
|
|
||||||
|
1. Navigate to the repository's "Releases" page
|
||||||
|
2. Click "Draft a new release"
|
||||||
|
3. Select the tag you just pushed
|
||||||
|
4. Title the release "GoMFT vX.Y.Z"
|
||||||
|
5. Copy the changelog entry for this version
|
||||||
|
6. Attach the built artifacts
|
||||||
|
7. Publish the release
|
||||||
|
|
||||||
|
### 2. Announce the Release
|
||||||
|
|
||||||
|
Announce the new release through:
|
||||||
|
|
||||||
|
- Project website
|
||||||
|
- GitHub Discussions
|
||||||
|
- Relevant community forums or mailing lists
|
||||||
|
|
||||||
|
### 3. Update Demo Environment
|
||||||
|
|
||||||
|
Update the demo/staging environment to the new version to showcase the latest features.
|
||||||
|
|
||||||
|
### 4. Version Bump for Development
|
||||||
|
|
||||||
|
Create a commit on the main branch that bumps the version to the next anticipated version with a `-dev` suffix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update versions in files to X.Y.(Z+1)-dev
|
||||||
|
git add VERSION package.json internal/version/version.go
|
||||||
|
git commit -m "Bump version to vX.Y.(Z+1)-dev"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hotfix Releases
|
||||||
|
|
||||||
|
For critical issues that need immediate fixes:
|
||||||
|
|
||||||
|
1. Create a hotfix branch from the release tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b hotfix/vX.Y.(Z+1) vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make the necessary fixes
|
||||||
|
|
||||||
|
3. Update version numbers and changelog
|
||||||
|
|
||||||
|
4. Follow the standard release process from the "Final Testing" step
|
||||||
|
|
||||||
|
## Long-Term Support (LTS)
|
||||||
|
|
||||||
|
- Major versions may be designated as LTS releases
|
||||||
|
- LTS releases receive security updates and critical bug fixes for 12 months after the next major version is released
|
||||||
|
- Only the most recent major version receives new features
|
||||||
|
|
||||||
|
## Release Checklist
|
||||||
|
|
||||||
|
Use this checklist for each release:
|
||||||
|
|
||||||
|
- [ ] All tests pass on the main branch
|
||||||
|
- [ ] Documentation is up-to-date
|
||||||
|
- [ ] CHANGELOG.md is updated
|
||||||
|
- [ ] Version numbers are updated in all files
|
||||||
|
- [ ] Release branch created (for minor/major versions)
|
||||||
|
- [ ] Final testing completed successfully
|
||||||
|
- [ ] Release committed and tagged
|
||||||
|
- [ ] Tag pushed to trigger build pipeline
|
||||||
|
- [ ] GitHub release created with changelog and artifacts
|
||||||
|
- [ ] Release announced to the community
|
||||||
|
- [ ] Demo environment updated
|
||||||
|
- [ ] Development version bumped on main branch
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
title: Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
GoMFT can be customized through environment variables. This document provides a complete list of configuration options available in GoMFT.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Environment variables are the primary way to configure GoMFT, especially when running in Docker. These variables can be set in your Docker Compose file, `.env` file, or directly in your system environment.
|
||||||
|
|
||||||
|
### Core Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Example |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| SERVER_ADDRESS | Server address and port | :8080 | `SERVER_ADDRESS=:8080` |
|
||||||
|
| DATA_DIR | Main data directory | ./data | `DATA_DIR=/app/data` |
|
||||||
|
| BACKUP_DIR | Directory for backups | ./backups | `BACKUP_DIR=/app/backups` |
|
||||||
|
| JWT_SECRET | Secret for JWT tokens | change_this_to_a_secure_random_string | `JWT_SECRET=your-secure-secret-key` |
|
||||||
|
| GOMFT_ENCRYPTION_KEY | Key used to encrypt sensitive data in the database | change_this_to_a_secure_random_string | `GOMFT_ENCRYPTION_KEY=your-secure-encryption-key` |
|
||||||
|
| BASE_URL | Base URL for GoMFT (used in email links) | http://localhost:8080 | `BASE_URL=https://gomft.example.com` |
|
||||||
|
| SKIP_SSL_VERIFY | Skip SSL verification for outgoing webhooks/notifications | false | `SKIP_SSL_VERIFY=false` |
|
||||||
|
|
||||||
|
### Authentication Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Example |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| TOTP_ENCRYPTION_KEY | Encryption key for TOTP secrets | this-is-a-dev-key-not-for-production! | `TOTP_ENCRYPTION_KEY=your-secure-key` |
|
||||||
|
| PUID | User ID to run as (Docker only) | | `PUID=1000` |
|
||||||
|
| PGID | Group ID to run as (Docker only) | | `PGID=1000` |
|
||||||
|
|
||||||
|
### Email Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Example |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| EMAIL_ENABLED | Enable email functionality | false | `EMAIL_ENABLED=true` |
|
||||||
|
| EMAIL_HOST | SMTP server hostname | smtp.example.com | `EMAIL_HOST=smtp.gmail.com` |
|
||||||
|
| EMAIL_PORT | SMTP server port | 587 | `EMAIL_PORT=587` |
|
||||||
|
| EMAIL_USERNAME | SMTP username | user@example.com | `EMAIL_USERNAME=your-email@example.com` |
|
||||||
|
| EMAIL_PASSWORD | SMTP password | your-password | `EMAIL_PASSWORD=your-smtp-password` |
|
||||||
|
| EMAIL_FROM_EMAIL | From email address | gomft@example.com | `EMAIL_FROM_EMAIL=gomft@example.com` |
|
||||||
|
| EMAIL_FROM_NAME | From name | GoMFT | `EMAIL_FROM_NAME=GoMFT Notifications` |
|
||||||
|
| EMAIL_REPLY_TO | Reply-to email address | | `EMAIL_REPLY_TO=support@example.com` |
|
||||||
|
| EMAIL_ENABLE_TLS | Use TLS for SMTP connection | true | `EMAIL_ENABLE_TLS=true` |
|
||||||
|
| EMAIL_REQUIRE_AUTH | Require authentication for SMTP | true | `EMAIL_REQUIRE_AUTH=true` |
|
||||||
|
|
||||||
|
### OAuth Configuration (Optional)
|
||||||
|
|
||||||
|
| Variable | Description | Default | Example |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| GOOGLE_CLIENT_ID | Google OAuth client ID | | `GOOGLE_CLIENT_ID=your-client-id` |
|
||||||
|
| GOOGLE_CLIENT_SECRET | Google OAuth client secret | | `GOOGLE_CLIENT_SECRET=your-client-secret` |
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
In addition to setting environment variables directly, GoMFT can also be configured using a `.env` file. This file should be placed in the root directory of your GoMFT installation.
|
||||||
|
|
||||||
|
Example `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_ADDRESS=:8080
|
||||||
|
DATA_DIR=./data
|
||||||
|
BACKUP_DIR=./backups
|
||||||
|
JWT_SECRET=change_this_to_a_secure_random_string
|
||||||
|
GOMFT_ENCRYPTION_KEY=change_this_to_a_secure_random_string
|
||||||
|
BASE_URL=http://localhost:8080
|
||||||
|
SKIP_SSL_VERIFY=false
|
||||||
|
|
||||||
|
# Two-Factor Authentication configuration
|
||||||
|
TOTP_ENCRYPTION_KEY=this-is-a-dev-key-not-for-production!
|
||||||
|
|
||||||
|
# OAuth Configuration (optional)
|
||||||
|
# GOOGLE_CLIENT_ID=your_google_client_id
|
||||||
|
# GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
EMAIL_ENABLED=true
|
||||||
|
EMAIL_HOST=smtp.example.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_FROM_EMAIL=gomft@example.com
|
||||||
|
EMAIL_FROM_NAME=GoMFT
|
||||||
|
EMAIL_REPLY_TO=
|
||||||
|
EMAIL_ENABLE_TLS=true
|
||||||
|
EMAIL_REQUIRE_AUTH=true
|
||||||
|
EMAIL_USERNAME=your-email@example.com
|
||||||
|
EMAIL_PASSWORD=your-smtp-password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
GoMFT uses the following priority order for configuration:
|
||||||
|
|
||||||
|
1. Environment variables set directly
|
||||||
|
2. Variables in the `.env` file
|
||||||
|
3. Default values
|
||||||
|
|
||||||
|
This means that environment variables set directly will override settings in the `.env` file, which in turn override the default values.
|
||||||
|
|
||||||
|
## Docker Configuration
|
||||||
|
|
||||||
|
When running GoMFT in Docker, you can configure the application in several ways:
|
||||||
|
|
||||||
|
### Using Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v /path/to/data:/app/data \
|
||||||
|
-v /path/to/backups:/app/backups \
|
||||||
|
-e SERVER_ADDRESS=:8080 \
|
||||||
|
-e JWT_SECRET=your-secure-secret \
|
||||||
|
-e GOMFT_ENCRYPTION_KEY=your-secure-encryption-key \
|
||||||
|
-e EMAIL_ENABLED=true \
|
||||||
|
-e EMAIL_HOST=smtp.example.com \
|
||||||
|
-e PUID=1000 \
|
||||||
|
-e PGID=1000 \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using .env File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v /path/to/data:/app/data \
|
||||||
|
-v /path/to/backups:/app/backups \
|
||||||
|
-v /path/to/.env:/app/.env \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
- ./.env:/app/.env # Mount .env file (optional)
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Applying Configuration Changes
|
||||||
|
|
||||||
|
Most configuration changes require a restart of the GoMFT service to take effect. After modifying environment variables or the `.env` file, restart your container or service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Docker
|
||||||
|
docker restart gomft
|
||||||
|
|
||||||
|
# For Docker Compose
|
||||||
|
docker-compose restart gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Docker Deployment](/docs/getting-started/docker) - Advanced Docker deployment options
|
||||||
|
- [Non-Root Operation](/docs/security/non-root) - Running GoMFT as a non-root user
|
||||||
|
- [Best Practices](/docs/security/best-practices) - Security best practices
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Docker Deployment
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker Deployment Guide
|
||||||
|
|
||||||
|
This guide provides detailed instructions for deploying GoMFT using Docker and Docker Compose, including advanced configuration options and best practices.
|
||||||
|
|
||||||
|
## Docker Image Information
|
||||||
|
|
||||||
|
GoMFT is available as a Docker image on Docker Hub:
|
||||||
|
|
||||||
|
- **Image Name**: `starfleetcptn/gomft`
|
||||||
|
- **Tags**:
|
||||||
|
- `latest` - Latest stable release
|
||||||
|
- `edge` - Latest development build
|
||||||
|
- `v0.1.0`, `v0.2.0`, etc. - Specific version releases
|
||||||
|
|
||||||
|
## Basic Docker Run Command
|
||||||
|
|
||||||
|
The simplest way to run GoMFT with Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v $(pwd)/data:/app/data \
|
||||||
|
-v $(pwd)/backups:/app/backups \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose Setup
|
||||||
|
|
||||||
|
For a more complete and production-ready setup, use Docker Compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Save this to a file named `docker-compose.yml` and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persisting Data
|
||||||
|
|
||||||
|
GoMFT stores data in specific directories that should be mounted as volumes:
|
||||||
|
|
||||||
|
- **/app/data**: Contains the SQLite database, rclone configurations, and logs
|
||||||
|
- **/app/backups**: Contains database backups
|
||||||
|
|
||||||
|
Example with more specific volume mapping:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./data/db:/app/data/db # Database files
|
||||||
|
- ./data/configs:/app/data/configs # Rclone config files
|
||||||
|
- ./data/logs:/app/data/logs # Log files
|
||||||
|
- ./backups:/app/backups # Backup files
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Transfer Volumes
|
||||||
|
|
||||||
|
In addition to the application data, you'll need to mount volumes for the files you want to transfer. These volumes provide GoMFT access to your source files and destination directories.
|
||||||
|
|
||||||
|
### Common File Volume Mounts
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
# Application data volumes
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
|
||||||
|
# File transfer volumes
|
||||||
|
- /path/to/source/files:/sftp/files # Source files for transfer
|
||||||
|
- /path/to/destination:/mft/destination # Destination for transferred files
|
||||||
|
- /path/to/temp:/mft/temp # Temporary processing directory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Run Example with File Volumes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v $(pwd)/data:/app/data \
|
||||||
|
-v $(pwd)/backups:/app/backups \
|
||||||
|
-v /path/to/source/files:/sftp/files \
|
||||||
|
-v /path/to/destination:/mft/destination \
|
||||||
|
-v /path/to/temp:/mft/temp \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose Example with File Volumes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
# Application data
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
|
||||||
|
# File transfer directories
|
||||||
|
- ./source_files:/sftp/files
|
||||||
|
- ./destination:/mft/destination
|
||||||
|
- ./temp:/mft/temp
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Permissions
|
||||||
|
|
||||||
|
When mounting file volumes, ensure the container has appropriate permissions to access these directories:
|
||||||
|
|
||||||
|
1. If using `PUID` and `PGID` environment variables:
|
||||||
|
```bash
|
||||||
|
# Set correct ownership on host directories
|
||||||
|
chown -R 1000:1000 /path/to/source/files
|
||||||
|
chown -R 1000:1000 /path/to/destination
|
||||||
|
chown -R 1000:1000 /path/to/temp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Or set appropriate permissions:
|
||||||
|
```bash
|
||||||
|
# Make directories accessible to the container
|
||||||
|
chmod -R 755 /path/to/source/files
|
||||||
|
chmod -R 755 /path/to/destination
|
||||||
|
chmod -R 755 /path/to/temp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
GoMFT can be configured using environment variables:
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- PORT=8080 # Web UI port
|
||||||
|
- BASE_URL=https://gomft.example.com # Base URL for email links
|
||||||
|
- TZ=America/New_York # Timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Directory Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- DATA_DIR=/app/data # Main data directory
|
||||||
|
- LOGS_DIR=/app/data/logs # Logs directory
|
||||||
|
- BACKUP_DIR=/app/backups # Backup directory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Notification Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- EMAIL_ENABLED=true # Enable email notifications
|
||||||
|
- EMAIL_HOST=smtp.example.com # SMTP server host
|
||||||
|
- EMAIL_PORT=587 # SMTP server port
|
||||||
|
- EMAIL_USER=user@example.com # SMTP username
|
||||||
|
- EMAIL_PASSWORD=password # SMTP password
|
||||||
|
- EMAIL_FROM=gomft@example.com # From address for emails
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- JWT_SECRET=your-secret-key # Secret for JWT tokens
|
||||||
|
- ENCRYPT_KEY=32-char-key # Key for encrypting sensitive data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running as Non-Root User
|
||||||
|
|
||||||
|
For enhanced security, run GoMFT as a non-root user:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- PUID=1000 # User ID to run as
|
||||||
|
- PGID=1000 # Group ID to run as
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure your mounted volumes have the appropriate permissions for this user.
|
||||||
|
|
||||||
|
## Exposing GoMFT Behind a Reverse Proxy
|
||||||
|
|
||||||
|
It's recommended to run GoMFT behind a reverse proxy like Nginx or Traefik for SSL termination and security.
|
||||||
|
|
||||||
|
### Nginx Example
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name gomft.example.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name gomft.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://gomft:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose with Traefik Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.5
|
||||||
|
command:
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.email=your@email.com"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.storage=/acme.json"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./acme.json:/acme.json
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- BASE_URL=https://gomft.example.com
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gomft.rule=Host(`gomft.example.com`)"
|
||||||
|
- "traefik.http.routers.gomft.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gomft.tls.certresolver=myresolver"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
You can configure a health check to monitor the container's health:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Limits
|
||||||
|
|
||||||
|
Set resource limits to prevent the container from consuming too many resources:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 512M
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging Configuration
|
||||||
|
|
||||||
|
Configure container logging:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting Docker Deployment
|
||||||
|
|
||||||
|
If you encounter issues with your Docker deployment:
|
||||||
|
|
||||||
|
1. Check container logs:
|
||||||
|
```
|
||||||
|
docker logs gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check container status:
|
||||||
|
```
|
||||||
|
docker ps -a | grep gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verify volume permissions:
|
||||||
|
```
|
||||||
|
ls -la ./data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Check container environment:
|
||||||
|
```
|
||||||
|
docker exec gomft env
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Inspect the container:
|
||||||
|
```
|
||||||
|
docker inspect gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, refer to the [GitHub repository](https://github.com/StarFleetCPTN/GoMFT) or open an issue.
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Installation
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installing GoMFT
|
||||||
|
|
||||||
|
GoMFT can be installed using Docker (recommended) or through a traditional installation. This guide covers both methods.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **CPU**: 1+ cores (2+ recommended for production)
|
||||||
|
- **RAM**: 512MB minimum (1GB+ recommended for production)
|
||||||
|
- **Disk Space**: 100MB for the application plus space for your transfer data and logs
|
||||||
|
- **Operating System**: Linux, macOS, or Windows with Docker support
|
||||||
|
|
||||||
|
## Docker Installation (Recommended)
|
||||||
|
|
||||||
|
The easiest way to deploy GoMFT is using Docker. This method handles all dependencies and provides an isolated environment.
|
||||||
|
|
||||||
|
### Using Docker Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v /path/to/data:/app/data \
|
||||||
|
-v /path/to/backups:/app/backups \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `/path/to/data` and `/path/to/backups` with your desired local paths for persistent storage.
|
||||||
|
|
||||||
|
### Using Docker Compose
|
||||||
|
|
||||||
|
Create a `docker-compose.yaml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
You can customize your GoMFT installation using environment variables:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PORT=8080
|
||||||
|
- DATA_DIR=/app/data
|
||||||
|
- BACKUP_DIR=/app/backups
|
||||||
|
- LOGS_DIR=/app/data/logs
|
||||||
|
- EMAIL_ENABLED=false
|
||||||
|
- BASE_URL=http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Configuration](/docs/getting-started/configuration) section for a complete list of environment variables.
|
||||||
|
|
||||||
|
### File Volume Mounts
|
||||||
|
|
||||||
|
When running GoMFT in Docker, you'll need to mount volumes to provide access to the files you want to transfer. Here are common volume mount scenarios:
|
||||||
|
|
||||||
|
#### For SFTP/FTP Source Files
|
||||||
|
```bash
|
||||||
|
-v /path/to/local/files:/sftp/files
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For Destination Directories
|
||||||
|
```bash
|
||||||
|
-v /path/to/destination:/mft/destination
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For Processing Temporary Files
|
||||||
|
```bash
|
||||||
|
-v /path/to/temp:/mft/temp
|
||||||
|
```
|
||||||
|
|
||||||
|
Example using Docker Run with file volumes:
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gomft \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-v /path/to/data:/app/data \
|
||||||
|
-v /path/to/backups:/app/backups \
|
||||||
|
-v /path/to/local/files:/sftp/files \
|
||||||
|
-v /path/to/destination:/mft/destination \
|
||||||
|
starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Example Docker Compose configuration with file volumes:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
- ./source_files:/sftp/files
|
||||||
|
- ./destination:/mft/destination
|
||||||
|
- ./temp:/mft/temp
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: Ensure the container has appropriate permissions to access the mounted directories. You may need to adjust host-side permissions accordingly.
|
||||||
|
|
||||||
|
## Traditional Installation
|
||||||
|
|
||||||
|
For environments where Docker is not available or preferred, you can install GoMFT directly.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.20 or later
|
||||||
|
- Node.js 18 or later
|
||||||
|
- gcc (for building SQLite dependencies)
|
||||||
|
- templ (for generating template code)
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/StarFleetCPTN/GoMFT.git
|
||||||
|
cd GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install Node.js dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the frontend assets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install templ if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Generate templ templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Build the Go application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Run the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying the Installation
|
||||||
|
|
||||||
|
After installation, access the GoMFT web interface by navigating to:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
The default login credentials are:
|
||||||
|
|
||||||
|
- **Username**: admin
|
||||||
|
- **Password**: admin
|
||||||
|
|
||||||
|
**Important**: Change the default password immediately after the first login for security reasons.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once GoMFT is installed, proceed to the [Quick Start](/docs/getting-started/quick-start) guide to begin configuring your file transfers.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Quick Start
|
||||||
|
---
|
||||||
|
|
||||||
|
# GoMFT Quick Start Guide
|
||||||
|
|
||||||
|
This guide will help you get up and running with GoMFT quickly. We'll cover logging in, creating your first connection configuration, and setting up a file transfer.
|
||||||
|
|
||||||
|
## Accessing the Web Interface
|
||||||
|
|
||||||
|
After installation, access the GoMFT web interface at `http://your-server:8080` (or the appropriate port if you've modified it).
|
||||||
|
|
||||||
|
1. Log in with the default credentials:
|
||||||
|
- **Username**: admin@example.com
|
||||||
|
- **Password**: admin
|
||||||
|
|
||||||
|
## Initial Dashboard
|
||||||
|
|
||||||
|
The dashboard provides an overview of:
|
||||||
|
- Recent transfer jobs
|
||||||
|
- Upcoming scheduled transfers
|
||||||
|
- System status
|
||||||
|
- Quick action buttons
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Creating Your First Transfer Configuration
|
||||||
|
|
||||||
|
1. Navigate to **Transfer Configurations** in the sidebar menu
|
||||||
|
2. Click **+ New Configuration**
|
||||||
|
3. Configure the transfer:
|
||||||
|
- Select source and destination configurations
|
||||||
|
- Specify source and destination paths
|
||||||
|
- Choose the transfer type (Copy, Sync, Move, etc.)
|
||||||
|
- Configure transfer options (file filtering, bandwidth limits, etc.)
|
||||||
|
4. Click **Save Transfer**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Create Your First Scheduled Job
|
||||||
|
1. Naviagate to **Scheduled Jobs** in the sidebar menu
|
||||||
|
2. Click **+ New Job**
|
||||||
|
3. Configure the job:
|
||||||
|
- Specifiy schedule
|
||||||
|
- Select Job(s) to run this can be 1 or more
|
||||||
|
- Change job run order if needed
|
||||||
|
|
||||||
|
## Running a Transfer
|
||||||
|
|
||||||
|
Once you've created a transfer configuration, you can:
|
||||||
|
|
||||||
|
### Run On-Demand
|
||||||
|
|
||||||
|
1. Navigate to **Secheduled Jobs**
|
||||||
|
2. Find your transfer in the list
|
||||||
|
3. Click the **Run Now** button
|
||||||
|
4. The transfer will execute immediately
|
||||||
|
|
||||||
|
### Schedule a Transfer
|
||||||
|
|
||||||
|
1. Navigate to **Schedules**
|
||||||
|
2. Click **Create New Schedule**
|
||||||
|
3. Select your transfer configuration
|
||||||
|
4. Set the schedule using cron syntax or the schedule builder
|
||||||
|
5. Set additional options (timeout, max retries, etc.)
|
||||||
|
6. Click **Save Schedule**
|
||||||
|
|
||||||
|
## Monitoring Transfers
|
||||||
|
|
||||||
|
1. Navigate to **Transfer History** to view all past and ongoing transfers
|
||||||
|
2. Click on a specific transfer to view detailed information:
|
||||||
|
- Transfer status
|
||||||
|
- Start and end times
|
||||||
|
- Files transferred
|
||||||
|
- Bytes transferred
|
||||||
|
- Errors (if any)
|
||||||
|
- Transfer log
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you've set up your first transfer, explore these additional features:
|
||||||
|
|
||||||
|
- [Docker Deployment](/docs/getting-started/docker) - For containerized deployment
|
||||||
|
- [Traditional Installation](/docs/getting-started/traditional) - For non-Docker environments
|
||||||
|
- [Transfer Concepts](/docs/core-concepts/transfers) - Learn more about transfer operations
|
||||||
|
- [Scheduling](/docs/core-concepts/schedules) - Advanced scheduling options
|
||||||
|
- [Monitoring](/docs/core-concepts/monitoring) - Advanced monitoring capabilities
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
title: Traditional Installation
|
||||||
|
---
|
||||||
|
|
||||||
|
# Traditional Installation Guide
|
||||||
|
|
||||||
|
This guide covers installing GoMFT directly on your system without using Docker. This approach is useful for environments where containers aren't available or when you need more direct control over the installation.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **Operating System**: Linux, macOS, or Windows
|
||||||
|
- **Go**: Version 1.20 or later
|
||||||
|
- **Node.js**: Version 18 or later
|
||||||
|
- **Build Tools**: gcc and related build tools (for SQLite compilation)
|
||||||
|
|
||||||
|
## Prerequisites Installation
|
||||||
|
|
||||||
|
### On Debian/Ubuntu Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Go
|
||||||
|
wget https://go.dev/dl/go1.20.linux-amd64.tar.gz
|
||||||
|
sudo tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz
|
||||||
|
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
|
||||||
|
source ~/.profile
|
||||||
|
|
||||||
|
# Install Node.js
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Install build tools
|
||||||
|
sudo apt-get install -y build-essential
|
||||||
|
```
|
||||||
|
|
||||||
|
### On macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Homebrew
|
||||||
|
brew install go
|
||||||
|
brew install node
|
||||||
|
brew install gcc
|
||||||
|
```
|
||||||
|
|
||||||
|
### On Windows
|
||||||
|
|
||||||
|
1. Install Go from [https://golang.org/dl/](https://golang.org/dl/)
|
||||||
|
2. Install Node.js from [https://nodejs.org/](https://nodejs.org/)
|
||||||
|
3. Install Build Tools for Visual Studio
|
||||||
|
|
||||||
|
## Building GoMFT from Source
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/StarFleetCPTN/GoMFT.git
|
||||||
|
cd GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install Node.js dependencies and build the frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Compile the Go application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o gomft main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation Options
|
||||||
|
|
||||||
|
### Option 1: Run Directly
|
||||||
|
|
||||||
|
After building, you can run the application directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Install as a System Service
|
||||||
|
|
||||||
|
#### On Linux (systemd)
|
||||||
|
|
||||||
|
Create a systemd service file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/systemd/system/gomft.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following content:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=GoMFT - Go Managed File Transfer
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=gomft
|
||||||
|
Group=gomft
|
||||||
|
WorkingDirectory=/opt/gomft
|
||||||
|
ExecStart=/opt/gomft/gomft
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
Environment="PORT=8080"
|
||||||
|
Environment="DATA_DIR=/var/lib/gomft/data"
|
||||||
|
Environment="BACKUP_DIR=/var/lib/gomft/backups"
|
||||||
|
Environment="LOGS_DIR=/var/log/gomft"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a dedicated user and set up directories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create user
|
||||||
|
sudo useradd -r -s /bin/false gomft
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
sudo mkdir -p /opt/gomft /var/lib/gomft/data /var/lib/gomft/backups /var/log/gomft
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
sudo cp -r * /opt/gomft/
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
sudo chown -R gomft:gomft /opt/gomft /var/lib/gomft /var/log/gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable and start the service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable gomft
|
||||||
|
sudo systemctl start gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On macOS (launchd)
|
||||||
|
|
||||||
|
Create a launchd plist file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /Library/LaunchDaemons/com.gomft.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following content:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.gomft</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/gomft/gomft</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/usr/local/gomft</string>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PORT</key>
|
||||||
|
<string>8080</string>
|
||||||
|
<key>DATA_DIR</key>
|
||||||
|
<string>/var/lib/gomft/data</string>
|
||||||
|
<key>BACKUP_DIR</key>
|
||||||
|
<string>/var/lib/gomft/backups</string>
|
||||||
|
<key>LOGS_DIR</key>
|
||||||
|
<string>/var/log/gomft</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
```
|
||||||
|
|
||||||
|
Set up directories and install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create directories
|
||||||
|
sudo mkdir -p /usr/local/gomft /var/lib/gomft/data /var/lib/gomft/backups /var/log/gomft
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
sudo cp -r * /usr/local/gomft/
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
sudo chown -R $(whoami):staff /usr/local/gomft /var/lib/gomft /var/log/gomft
|
||||||
|
|
||||||
|
# Load service
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.gomft.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Windows (Windows Service)
|
||||||
|
|
||||||
|
1. Install [NSSM (Non-Sucking Service Manager)](https://nssm.cc/download)
|
||||||
|
2. Open Command Prompt as Administrator
|
||||||
|
3. Create the service:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
nssm install GoMFT C:\path\to\gomft.exe
|
||||||
|
nssm set GoMFT AppDirectory C:\path\to\gomft\directory
|
||||||
|
nssm set GoMFT AppEnvironmentExtra PORT=8080 DATA_DIR=C:\ProgramData\GoMFT\data BACKUP_DIR=C:\ProgramData\GoMFT\backups LOGS_DIR=C:\ProgramData\GoMFT\logs
|
||||||
|
nssm start GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the application directory or set system environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
PORT=8080
|
||||||
|
DATA_DIR=/var/lib/gomft/data
|
||||||
|
BACKUP_DIR=/var/lib/gomft/backups
|
||||||
|
LOGS_DIR=/var/log/gomft
|
||||||
|
BASE_URL=http://localhost:8080
|
||||||
|
EMAIL_ENABLED=false
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
ENCRYPT_KEY=32-character-encryption-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Server Setup
|
||||||
|
|
||||||
|
For production use, it's recommended to run GoMFT behind a web server like Nginx:
|
||||||
|
|
||||||
|
#### Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-gomft-server.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating GoMFT
|
||||||
|
|
||||||
|
To update a traditionally installed GoMFT:
|
||||||
|
|
||||||
|
1. Stop the service:
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop gomft # For Linux
|
||||||
|
sudo launchctl unload /Library/LaunchDaemons/com.gomft.plist # For macOS
|
||||||
|
nssm stop GoMFT # For Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Back up your data:
|
||||||
|
```bash
|
||||||
|
cp -r /var/lib/gomft/data /var/lib/gomft/data.backup
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Get the latest code:
|
||||||
|
```bash
|
||||||
|
cd /path/to/gomft/source
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Rebuild:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
go build -o gomft main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update the installation:
|
||||||
|
```bash
|
||||||
|
sudo cp gomft /opt/gomft/ # For Linux
|
||||||
|
sudo cp gomft /usr/local/gomft/ # For macOS
|
||||||
|
copy gomft.exe C:\path\to\gomft.exe # For Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Restart the service:
|
||||||
|
```bash
|
||||||
|
sudo systemctl start gomft # For Linux
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.gomft.plist # For macOS
|
||||||
|
nssm start GoMFT # For Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Errors**:
|
||||||
|
- Check that the user running GoMFT has write permissions to the data, backup, and logs directories.
|
||||||
|
|
||||||
|
2. **Database Errors**:
|
||||||
|
- Ensure the SQLite database path is writeable.
|
||||||
|
- Check database integrity: `sqlite3 /var/lib/gomft/data/gomft.db "PRAGMA integrity_check;"`
|
||||||
|
|
||||||
|
3. **Port Already in Use**:
|
||||||
|
- Change the port in the configuration.
|
||||||
|
- Check what's using port 8080: `sudo lsof -i :8080`
|
||||||
|
|
||||||
|
4. **Missing Dependencies**:
|
||||||
|
- Make sure all required Go and Node.js dependencies are installed.
|
||||||
|
|
||||||
|
### Viewing Logs
|
||||||
|
|
||||||
|
- **Application Logs**: Check `/var/log/gomft/` or your configured logs directory
|
||||||
|
- **System Service Logs**:
|
||||||
|
```bash
|
||||||
|
# For Linux
|
||||||
|
journalctl -u gomft
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
log show --predicate 'senderImagePath contains "gomft"'
|
||||||
|
|
||||||
|
# For Windows
|
||||||
|
Get-EventLog -LogName Application -Source GoMFT
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, refer to the [GitHub repository](https://github.com/StarFleetCPTN/GoMFT) or open an issue.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Features
|
||||||
|
---
|
||||||
|
|
||||||
|
# GoMFT Features
|
||||||
|
|
||||||
|
GoMFT offers a comprehensive set of features that make it a powerful solution for managed file transfers. Here's a detailed breakdown of what GoMFT offers:
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### Multi-Protocol Support
|
||||||
|
|
||||||
|
GoMFT leverages rclone to support a wide range of storage providers and protocols:
|
||||||
|
|
||||||
|
- **Cloud Storage**: Amazon S3, Google Cloud Storage
|
||||||
|
- **Object Storage**: MinIO, Backblaze B2, Wasabi
|
||||||
|
- **FTP/SFTP**: FTP, FTPS, SFTP servers
|
||||||
|
- **WebDAV**: WebDAV servers and services
|
||||||
|
- **Local Storage**: Local disk, SMB/CIFS shares
|
||||||
|
- **And many more**: Over 40 storage systems supported
|
||||||
|
|
||||||
|
### Intuitive Web Interface
|
||||||
|
|
||||||
|
- **Clean, Modern UI**: Easy-to-use web interface built with Tailwind CSS and HTMX
|
||||||
|
- **Dashboard**: Overview of recent transfers, scheduled jobs, and system status
|
||||||
|
- **Configuration Manager**: Visual interface for creating and editing transfer configurations
|
||||||
|
- **Job Scheduler**: Interface for creating and managing scheduled jobs
|
||||||
|
- **Transfer Logs**: Detailed logs of all transfer operations
|
||||||
|
- **Dark Mode**: Support for light and dark themes
|
||||||
|
|
||||||
|
### Powerful Scheduling
|
||||||
|
|
||||||
|
- **Cron-style Scheduling**: Set up transfers using familiar cron syntax
|
||||||
|
- **Recurring Transfers**: Schedule transfers to run on a regular basis
|
||||||
|
- **One-time Transfers**: Run transfers immediately or at a specific time
|
||||||
|
- **Schedule Grouping**: Organize schedules into logical groups
|
||||||
|
- **Priority Control**: Set priority levels for scheduled tasks
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Transfer Options
|
||||||
|
|
||||||
|
- **Bidirectional Sync**: Synchronize files in both directions
|
||||||
|
- **File Filtering**: Include or exclude files based on patterns
|
||||||
|
- **Bandwidth Limiting**: Restrict bandwidth usage for transfers
|
||||||
|
- **Parallel Transfers**: Configure the number of simultaneous transfers
|
||||||
|
- **Delta Transfers**: Transfer only changed parts of files
|
||||||
|
- **Checksumming**: Verify file integrity during transfers
|
||||||
|
|
||||||
|
### Notification System
|
||||||
|
|
||||||
|
- **Notifications**: Receive alerts when transfers complete or fail
|
||||||
|
- **Custom Templates**: Customize notification content and format
|
||||||
|
- **Notification Rules**: Configure which events trigger notifications
|
||||||
|
- **Notification Providers**: Webhooks, Ntfy, Gotify, Pushover, Pushbullet
|
||||||
|
|
||||||
|
### Admin Tools
|
||||||
|
|
||||||
|
- **User Management**: Create and manage users with different roles
|
||||||
|
- **Role-Based Access Control**: Control access to different parts of the application
|
||||||
|
- **Audit Logging**: Track user actions for security and compliance
|
||||||
|
- **System Monitoring**: Monitor system performance and resource usage
|
||||||
|
- **Database Backup/Restore**: Back up and restore the application database
|
||||||
|
- **Log Viewer**: Browse and search through application logs
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
|
||||||
|
- **Authentication**: Secure login with optional MFA support
|
||||||
|
- **Encryption**: Encrypt data in transit and at rest
|
||||||
|
- **Secure Credential Storage**: Safely store connection credentials
|
||||||
|
- **Non-Root Container Support**: Run containers as non-root users for enhanced security
|
||||||
|
|
||||||
|
## Integration Capabilities
|
||||||
|
|
||||||
|
- **Docker Support**: Easy deployment with Docker containers
|
||||||
|
- **Docker Compose**: Multi-container deployment using Docker Compose
|
||||||
|
- **Reverse Proxy Compatible**: Works behind reverse proxies like Nginx or Traefik
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Overview
|
||||||
|
---
|
||||||
|
|
||||||
|
# GoMFT Overview
|
||||||
|
|
||||||
|
GoMFT is a modern, web-based managed file transfer solution written in Go. It provides an intuitive interface for setting up, scheduling, and monitoring file transfers across various storage backends.
|
||||||
|
|
||||||
|
## What is GoMFT?
|
||||||
|
|
||||||
|
GoMFT (Go Managed File Transfer) is an open-source file transfer platform that enables reliable, secure, and automated file transfers. Built on top of the powerful [rclone](https://rclone.org/) engine, GoMFT provides a user-friendly web interface that makes it easy to configure and manage complex file transfer operations.
|
||||||
|
|
||||||
|
## Key Benefits
|
||||||
|
|
||||||
|
- **User-Friendly Interface**: Intuitive web UI for configuring and monitoring file transfers
|
||||||
|
- **Multi-Protocol Support**: Transfer files using SFTP, S3, Google Drive, and many more protocols
|
||||||
|
- **Automated Scheduling**: Set up recurring transfers with flexible scheduling options
|
||||||
|
- **Comprehensive Logging**: Detailed logs for troubleshooting and audit purposes
|
||||||
|
- **Notifications**: Get alerts when transfers succeed or fail
|
||||||
|
- **Docker Support**: Easy deployment with Docker containers
|
||||||
|
- **Security**: Role-based access control and secure credential management
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Data Synchronization**: Keep files in sync across different storage systems
|
||||||
|
- **Backup and Archiving**: Automate backup processes to cloud or local storage
|
||||||
|
- **Secure File Exchange**: Transfer files securely between organizations
|
||||||
|
- **Cloud Migration**: Move data between different cloud providers
|
||||||
|
- **Workflow Automation**: Trigger file transfers as part of larger workflows
|
||||||
|
- **Compliance**: Maintain audit logs for regulatory compliance
|
||||||
|
|
||||||
|
GoMFT is designed to be simple to deploy and use, while providing the reliability and features needed for enterprise file transfer needs.
|
||||||
|
|
||||||
|
## Community Support
|
||||||
|
|
||||||
|
Join our Discord community for support, discussions, and updates about GoMFT:
|
||||||
|
|
||||||
|
<a href="https://discord.gg/f9dwtM3j" className="discord-badge">Join Discord Community</a>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Authentication
|
||||||
|
---
|
||||||
|
|
||||||
|
# Authentication and Authorization
|
||||||
|
|
||||||
|
GoMFT provides robust authentication and authorization mechanisms to ensure secure access to the application and its features. This page explains how to configure and manage authentication in GoMFT.
|
||||||
|
|
||||||
|
## Authentication Methods
|
||||||
|
|
||||||
|
GoMFT supports multiple authentication methods to secure access to the application:
|
||||||
|
|
||||||
|
### Local Authentication
|
||||||
|
|
||||||
|
The default authentication method using GoMFT's built-in user database:
|
||||||
|
|
||||||
|
- **Username/Password**: Traditional username and password authentication
|
||||||
|
- **Password Requirements**: Configurable password complexity rules
|
||||||
|
- **Password Expiration**: Force password changes after a configurable period
|
||||||
|
- **Account Lockout**: Temporarily lock accounts after failed login attempts
|
||||||
|
|
||||||
|
### OAuth/OpenID Connect
|
||||||
|
|
||||||
|
Support for modern identity providers:
|
||||||
|
|
||||||
|
- **Single Sign-On**: Integrate with SSO solutions
|
||||||
|
- **Identity Providers**: Support for popular providers (Google, Microsoft, Okta, etc.)
|
||||||
|
- **JWT Tokens**: Secure token-based authentication
|
||||||
|
- **Automatic Account Provisioning (COMING SOON)**: Create GoMFT accounts based on SSO information
|
||||||
|
|
||||||
|
## Setting Up Authentication
|
||||||
|
|
||||||
|
### Configuring Local Authentication
|
||||||
|
|
||||||
|
Local authentication is enabled by default and requires minimal setup:
|
||||||
|
|
||||||
|
### Configuring OAuth/OpenID Connect
|
||||||
|
|
||||||
|
To set up OAuth or OpenID Connect:
|
||||||
|
|
||||||
|
1. Navigate to **Settingss** > **Authentication Providers**
|
||||||
|
2. Select **OAuth/OIDC** as an authentication method
|
||||||
|
3. Configure provider settings:
|
||||||
|
- Provider URL
|
||||||
|
- Client ID
|
||||||
|
- Client Secret
|
||||||
|
- Scope (e.g., `openid profile email`)
|
||||||
|
- Callback URL
|
||||||
|
4. Set up attribute mappings:
|
||||||
|
- Map provider attributes to GoMFT user properties
|
||||||
|
- Configure role attribute or claim
|
||||||
|
5. Test the configuration
|
||||||
|
|
||||||
|
## Multi-Factor Authentication (MFA)
|
||||||
|
|
||||||
|
GoMFT supports multi-factor authentication for enhanced security:
|
||||||
|
|
||||||
|
### MFA Options
|
||||||
|
|
||||||
|
- **Time-based One-Time Password (TOTP)**: Compatible with apps like Google Authenticator
|
||||||
|
- **Email Verification Codes**: One-time codes sent via email
|
||||||
|
- **Recovery Codes**: Backup codes for emergency access
|
||||||
|
|
||||||
|
### Enabling MFA
|
||||||
|
|
||||||
|
For users to set up MFA:
|
||||||
|
|
||||||
|
1. Log in to GoMFT
|
||||||
|
2. Navigate to **Profile** > **Security Settings**
|
||||||
|
3. Select **Enable Multi-Factor Authentication**
|
||||||
|
4. Choose the MFA method (e.g., TOTP)
|
||||||
|
5. Follow the setup instructions:
|
||||||
|
- For TOTP: Scan QR code with authenticator app
|
||||||
|
- For Email: Verify email address
|
||||||
|
6. Generate and save recovery codes
|
||||||
|
|
||||||
|
## User Management
|
||||||
|
|
||||||
|
### Creating Users
|
||||||
|
|
||||||
|
To create new users:
|
||||||
|
|
||||||
|
1. Navigate to **Administration** > **Users**
|
||||||
|
2. Click **Create New User**
|
||||||
|
3. Fill in the user details:
|
||||||
|
- Username
|
||||||
|
- Email address
|
||||||
|
- Full name
|
||||||
|
- Initial password or send password reset link
|
||||||
|
- Role assignment
|
||||||
|
4. Click **Create Users**
|
||||||
|
|
||||||
|
### Managing User Accounts
|
||||||
|
|
||||||
|
To manage existing users:
|
||||||
|
|
||||||
|
1. Navigate to **Administration** > **Users**
|
||||||
|
2. Find the user in the list
|
||||||
|
3. Available actions:
|
||||||
|
- Edit user details
|
||||||
|
- Change role assignment
|
||||||
|
- Reset password
|
||||||
|
- Enable/disable account
|
||||||
|
- Force MFA enrollment
|
||||||
|
- Delete user
|
||||||
|
|
||||||
|
### User Self-Service
|
||||||
|
|
||||||
|
GoMFT provides self-service features for users:
|
||||||
|
|
||||||
|
- **Profile Management**: Users can update their profile information
|
||||||
|
- **Password Change**: Users can change their password
|
||||||
|
- **MFA Setup**: Users can configure their MFA preferences
|
||||||
|
|
||||||
|
## Role-Based Access Control
|
||||||
|
|
||||||
|
GoMFT implements role-based access control (RBAC) to manage permissions:
|
||||||
|
|
||||||
|
### Default Roles
|
||||||
|
|
||||||
|
- **Administrator**: Full access to all system features
|
||||||
|
- **System**: Can manage transfers and connections but not admin settings
|
||||||
|
- **User**: Basic access to create and manage personal transfers
|
||||||
|
|
||||||
|
### Creating Custom Roles
|
||||||
|
|
||||||
|
To create a custom role:
|
||||||
|
|
||||||
|
1. Navigate to **Administration** > **Roles**
|
||||||
|
2. Click **Create New Role**
|
||||||
|
3. Define the role:
|
||||||
|
- Role name
|
||||||
|
- Description
|
||||||
|
- Permission assignments
|
||||||
|
4. Save the role
|
||||||
|
|
||||||
|
### Permission Categories
|
||||||
|
|
||||||
|
GoMFT organizes permissions into categories:
|
||||||
|
|
||||||
|
- **System Administration**: System-wide settings and maintenance
|
||||||
|
- **User Management**: User and role administration
|
||||||
|
- **Transfer Management**: Creating and managing transfers
|
||||||
|
- **Connection Management**: Creating and managing connections
|
||||||
|
- **Schedule Management**: Managing transfer schedules
|
||||||
|
- **Execution Control**: Running and controlling transfers
|
||||||
|
- **Monitoring**: Viewing logs and reports
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
- **Enforce Strong Passwords**: Configure strong password requirements
|
||||||
|
- **Enable MFA**: Require MFA for all users, especially administrators
|
||||||
|
- **Regular Review**: Periodically review user accounts and permissions
|
||||||
|
- **Principle of Least Privilege**: Assign the minimum necessary permissions
|
||||||
|
- **Audit Authentication**: Monitor and audit authentication events
|
||||||
|
- **Secure Configuration**: Properly secure authentication configuration files
|
||||||
|
- **Account Lifecycle**: Implement processes for account creation and termination
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Security Best Practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Best Practices
|
||||||
|
|
||||||
|
This guide provides recommendations for securing your GoMFT installation and maintaining a secure file transfer environment.
|
||||||
|
|
||||||
|
## Installation Security
|
||||||
|
|
||||||
|
### Use Docker Security Features
|
||||||
|
|
||||||
|
When deploying GoMFT with Docker:
|
||||||
|
|
||||||
|
- **Run as Non-Root**: Always run the container as a non-root user (see [Running as Non-Root](/docs/security/non-root))
|
||||||
|
- **Use Read-Only Filesystem**: Mount the filesystem as read-only except for specific data directories
|
||||||
|
- **Limit Capabilities**: Use Docker's `--cap-drop` to limit container capabilities
|
||||||
|
- **Set Resource Limits**: Prevent resource exhaustion with memory and CPU limits
|
||||||
|
- **Use Docker Secrets**: Store sensitive configuration in Docker secrets instead of environment variables
|
||||||
|
|
||||||
|
Example secure docker-compose configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
user: "1000:1000"
|
||||||
|
read_only: true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traditional Installation Security
|
||||||
|
|
||||||
|
For traditional installations:
|
||||||
|
|
||||||
|
- **Dedicated User**: Create a dedicated system user for running GoMFT
|
||||||
|
- **Minimal Permissions**: Give the user only the permissions it needs
|
||||||
|
- **Firewall Rules**: Restrict access to only necessary ports
|
||||||
|
- **SELinux/AppArmor**: Use system security modules to limit application scope
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
### Use HTTPS
|
||||||
|
|
||||||
|
Always use HTTPS for the web interface:
|
||||||
|
|
||||||
|
- **Configure TLS**: Use a reverse proxy like Nginx or Traefik for TLS termination
|
||||||
|
- **Strong Ciphers**: Use modern, secure cipher suites
|
||||||
|
- **HSTS**: Enable HTTP Strict Transport Security
|
||||||
|
- **Valid Certificates**: Use trusted certificates from Let's Encrypt or other providers
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
- **IP Restrictions**: Limit access to trusted IP addresses where possible
|
||||||
|
- **VPN Access**: Consider placing GoMFT behind a VPN for additional security
|
||||||
|
- **Firewall Rules**: Configure firewall rules to restrict access to essential ports only
|
||||||
|
|
||||||
|
## Authentication and Authorization
|
||||||
|
|
||||||
|
### Strong Authentication
|
||||||
|
|
||||||
|
- **Password Policy**: Enforce strong password requirements
|
||||||
|
- **MFA**: Enable Multi-Factor Authentication for all users
|
||||||
|
- **Session Management**: Set appropriate session timeouts
|
||||||
|
- **Failed Login Limits**: Implement account lockouts after several failed attempts
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
- **Principle of Least Privilege**: Grant users only the permissions they need
|
||||||
|
- **Separation of Duties**: Use roles to separate administrative functions
|
||||||
|
- **Regular Review**: Periodically review user roles and permissions
|
||||||
|
|
||||||
|
## Credential Management
|
||||||
|
|
||||||
|
### Secure Storage
|
||||||
|
|
||||||
|
- **Encrypted Credentials**: Ensure all credentials are encrypted at rest
|
||||||
|
- **Isolated Storage**: Store sensitive credentials in a separate database or secure storage
|
||||||
|
- **Key Rotation**: Regularly rotate encryption keys
|
||||||
|
|
||||||
|
### Credential Practices
|
||||||
|
|
||||||
|
- **Service Accounts**: Use service accounts instead of personal accounts for connections
|
||||||
|
- **Temporary Credentials**: Use temporary credentials where supported (e.g., AWS STS)
|
||||||
|
- **API Keys**: Regularly rotate API keys and access tokens
|
||||||
|
- **Minimal Scope**: Grant credentials the minimum required permissions
|
||||||
|
|
||||||
|
## Transfer Security
|
||||||
|
|
||||||
|
### Secure Protocols
|
||||||
|
|
||||||
|
- **Choose Secure Protocols**: Prefer SFTP, FTPS, or HTTPS over unencrypted protocols
|
||||||
|
- **Disable Legacy Protocols**: Disable insecure protocols like FTP where possible
|
||||||
|
- **Strong Ciphers**: Configure secure cipher suites for encrypted protocols
|
||||||
|
|
||||||
|
### Data Handling
|
||||||
|
|
||||||
|
- **Data Classification**: Classify data by sensitivity and apply appropriate controls
|
||||||
|
- **Data Validation**: Validate files before processing them
|
||||||
|
- **Virus Scanning**: Implement virus scanning for transferred files
|
||||||
|
- **Data Loss Prevention**: Consider DLP measures for sensitive data
|
||||||
|
|
||||||
|
## Auditing and Monitoring
|
||||||
|
|
||||||
|
### Comprehensive Logging
|
||||||
|
|
||||||
|
- **Detailed Logs**: Enable detailed logging for all operations
|
||||||
|
- **Secure Log Storage**: Store logs securely with access controls
|
||||||
|
- **Log Rotation**: Implement log rotation to manage disk space
|
||||||
|
- **Tamper Protection**: Ensure logs cannot be modified or deleted
|
||||||
|
|
||||||
|
### Monitoring and Alerting
|
||||||
|
|
||||||
|
- **Real-time Monitoring**: Monitor for suspicious activities
|
||||||
|
- **Security Alerts**: Configure alerts for security-related events
|
||||||
|
- **Performance Monitoring**: Watch for performance issues that might indicate attacks
|
||||||
|
- **Regular Review**: Establish a process for regular log review
|
||||||
|
|
||||||
|
## System Security
|
||||||
|
|
||||||
|
### Regular Updates
|
||||||
|
|
||||||
|
- **Update GoMFT**: Keep GoMFT updated to the latest version
|
||||||
|
- **Patch Host System**: Keep the host operating system patched
|
||||||
|
- **Update Dependencies**: Keep all dependencies (Docker, etc.) updated
|
||||||
|
|
||||||
|
### Backup and Recovery
|
||||||
|
|
||||||
|
- **Regular Backups**: Back up the GoMFT database and configurations regularly
|
||||||
|
- **Secure Backups**: Encrypt backups and store them securely
|
||||||
|
- **Test Restoration**: Regularly test backup restoration
|
||||||
|
- **Disaster Recovery Plan**: Create and maintain a disaster recovery plan
|
||||||
|
|
||||||
|
## Periodic Security Review
|
||||||
|
|
||||||
|
### Security Assessments
|
||||||
|
|
||||||
|
- **Vulnerability Scanning**: Regularly scan for vulnerabilities
|
||||||
|
- **Penetration Testing**: Conduct periodic penetration tests
|
||||||
|
- **Configuration Review**: Review security configurations regularly
|
||||||
|
- **Compliance Checks**: Ensure ongoing compliance with relevant standards
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **Security Policies**: Document security policies and procedures
|
||||||
|
- **Configuration Documentation**: Maintain documentation of secure configurations
|
||||||
|
- **Incident Response Plan**: Create and maintain an incident response plan
|
||||||
|
|
||||||
|
## Integrating with Security Tools
|
||||||
|
|
||||||
|
GoMFT can be integrated with external security tools:
|
||||||
|
|
||||||
|
- **SIEM Integration**: Forward logs to Security Information and Event Management tools
|
||||||
|
- **Vulnerability Scanners**: Include GoMFT in vulnerability scanning
|
||||||
|
- **Compliance Tools**: Integrate with compliance monitoring tools
|
||||||
|
|
||||||
|
## Best Practices for Specific Environments
|
||||||
|
|
||||||
|
### Cloud Deployment
|
||||||
|
|
||||||
|
- **Cloud Security Services**: Utilize cloud provider security services
|
||||||
|
- **Network Security Groups**: Configure appropriate network security groups
|
||||||
|
- **Private Endpoints**: Use private endpoints where possible
|
||||||
|
- **Cloud IAM**: Leverage cloud Identity and Access Management
|
||||||
|
|
||||||
|
### On-Premises Deployment
|
||||||
|
|
||||||
|
- **Network Segmentation**: Place GoMFT in an appropriate network segment
|
||||||
|
- **Physical Security**: Ensure physical security of the servers
|
||||||
|
- **Environmental Controls**: Implement appropriate environmental controls
|
||||||
|
- **Backup Power**: Ensure backup power for critical systems
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Running as Non-Root
|
||||||
|
---
|
||||||
|
|
||||||
|
# Running GoMFT as a Non-Root User
|
||||||
|
|
||||||
|
By default, Docker containers run as the root user, which can pose security risks. GoMFT fully supports running as a non-root user, which is recommended for production environments.
|
||||||
|
|
||||||
|
## Benefits of Running as Non-Root
|
||||||
|
|
||||||
|
- **Improved Security**: Limits the potential damage if the container is compromised
|
||||||
|
- **Better File Permissions**: Files created by the container will match your host user permissions
|
||||||
|
- **Compliance**: Many security policies and best practices require containers to run as non-root
|
||||||
|
|
||||||
|
## Methods to Run GoMFT as Non-Root
|
||||||
|
|
||||||
|
GoMFT supports several methods for running as a non-root user, each with its own advantages.
|
||||||
|
|
||||||
|
### Method 1: Using PUID/PGID Environment Variables (Recommended)
|
||||||
|
|
||||||
|
This method allows changing the user at runtime without rebuilding the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using current user's ID
|
||||||
|
docker run -e PUID=$(id -u) -e PGID=$(id -g) starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in docker-compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
environment:
|
||||||
|
- PUID=1000 # Your user ID
|
||||||
|
- PGID=1000 # Your group ID
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using the `--user` Flag with Docker Run
|
||||||
|
|
||||||
|
This method is simple but doesn't support some advanced features like permission fixing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --user $(id -u):$(id -g) starfleetcptn/gomft:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Using Docker Compose with Environment Variables
|
||||||
|
|
||||||
|
This approach uses environment variables from the host for the user directive:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
UID=$(id -u) GID=$(id -g) docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 4: Building a Custom Image with Specified UID/GID
|
||||||
|
|
||||||
|
This method builds a custom image with your specified user ID:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM starfleetcptn/gomft:latest
|
||||||
|
|
||||||
|
ARG UID=1000
|
||||||
|
ARG GID=1000
|
||||||
|
|
||||||
|
RUN usermod -u $UID gomft && groupmod -g $GID gomft
|
||||||
|
```
|
||||||
|
|
||||||
|
In docker-compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
UID: ${UID:-1000}
|
||||||
|
GID: ${GID:-1000}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables for User Management
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| -------- | ------------------ | ------------------------ |
|
||||||
|
| PUID | User ID to run as | Built-in user ID (1000) |
|
||||||
|
| PGID | Group ID to run as | Built-in group ID (1000) |
|
||||||
|
| USERNAME | Username to use | gomft |
|
||||||
|
|
||||||
|
## Volume Permissions
|
||||||
|
|
||||||
|
When running as a non-root user, ensure that the directories on the host have appropriate permissions for the container user:
|
||||||
|
|
||||||
|
### Option 1: Create Directories with Correct Ownership (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create directories
|
||||||
|
mkdir -p data backups
|
||||||
|
|
||||||
|
# Set ownership to match the PUID/PGID you'll use
|
||||||
|
chown -R 1000:1000 data backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Adjust Permissions (Less Secure, but Easier for Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data backups
|
||||||
|
chmod -R 777 data backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying Non-Root Operation
|
||||||
|
|
||||||
|
To verify that GoMFT is running as a non-root user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec gomft id
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output showing the UID and GID you specified.
|
||||||
|
|
||||||
|
## Troubleshooting Permission Issues
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Volume Mount Permission Denied**: The container user doesn't have permission to access mounted volumes
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
chown -R <PUID>:<PGID> ./data ./backups
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Cannot Write to Log Files**: Permission issues with log files
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Ensure log directory exists and has correct permissions
|
||||||
|
mkdir -p ./data/logs
|
||||||
|
chown -R <PUID>:<PGID> ./data/logs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database Permission Errors**: SQLite database permissions
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Check and fix database file permissions
|
||||||
|
chown <PUID>:<PGID> ./data/gomft.db
|
||||||
|
chmod 644 ./data/gomft.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Container Logs for Permission Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs gomft | grep -i "permission denied"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Permission Script
|
||||||
|
|
||||||
|
You can use this script to fix permissions on your data volumes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Fix permissions for GoMFT volumes
|
||||||
|
|
||||||
|
# Set your PUID and PGID here
|
||||||
|
PUID=1000
|
||||||
|
PGID=1000
|
||||||
|
|
||||||
|
# Create directories if they don't exist
|
||||||
|
mkdir -p ./data ./backups
|
||||||
|
|
||||||
|
# Fix ownership
|
||||||
|
chown -R $PUID:$PGID ./data ./backups
|
||||||
|
|
||||||
|
echo "Permissions fixed for GoMFT volumes"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
When running as non-root, there are still some security considerations:
|
||||||
|
|
||||||
|
- Avoid using `chmod 777` in production environments
|
||||||
|
- Use volume binding with caution, especially for sensitive data
|
||||||
|
- Consider using Docker secrets for sensitive credentials
|
||||||
|
- Regularly update your GoMFT image to get the latest security fixes
|
||||||
|
- Implement network segmentation to limit the container's access
|
||||||
|
|
||||||
|
## Example: Complete Docker Compose Setup with Non-Root User
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gomft:
|
||||||
|
image: starfleetcptn/gomft:latest
|
||||||
|
container_name: gomft
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=UTC
|
||||||
|
- BASE_URL=http://localhost:8080
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./backups:/app/backups
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices Summary
|
||||||
|
|
||||||
|
1. **Always run GoMFT as a non-root user in production**
|
||||||
|
2. **Use PUID/PGID environment variables for flexible user mapping**
|
||||||
|
3. **Set appropriate permissions on volume mounts**
|
||||||
|
4. **Verify the container is running as the expected user**
|
||||||
|
5. **Follow least privilege principles for the container user**
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
---
|
||||||
|
id: storage-provider-guide
|
||||||
|
title: Storage Provider Guide
|
||||||
|
sidebar_label: Storage Providers
|
||||||
|
description: Detailed instructions for using the Storage Provider feature in GoMFT
|
||||||
|
---
|
||||||
|
|
||||||
|
# Storage Provider User Guide
|
||||||
|
|
||||||
|
This guide provides detailed instructions for using the new Storage Provider feature in GoMFT.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Introduction](#introduction)
|
||||||
|
2. [Managing Storage Providers](#managing-storage-providers)
|
||||||
|
- [Viewing Your Storage Providers](#viewing-your-storage-providers)
|
||||||
|
- [Creating a New Storage Provider](#creating-a-new-storage-provider)
|
||||||
|
- [Editing Storage Providers](#editing-storage-providers)
|
||||||
|
- [Testing Connections](#testing-connections)
|
||||||
|
- [Deleting Storage Providers](#deleting-storage-providers)
|
||||||
|
3. [Using Storage Providers in Transfers](#using-storage-providers-in-transfers)
|
||||||
|
- [Creating Transfers with Storage Providers](#creating-transfers-with-storage-providers)
|
||||||
|
- [Converting Existing Transfers](#converting-existing-transfers)
|
||||||
|
4. [Provider Type Reference](#provider-type-reference)
|
||||||
|
- [SFTP Configuration](#sftp-configuration)
|
||||||
|
- [S3 Configuration](#s3-configuration)
|
||||||
|
- [OneDrive Configuration](#onedrive-configuration)
|
||||||
|
- [Google Drive Configuration](#google-drive-configuration)
|
||||||
|
- [FTP Configuration](#ftp-configuration)
|
||||||
|
- [SMB Configuration](#smb-configuration)
|
||||||
|
5. [Troubleshooting](#troubleshooting)
|
||||||
|
- [Common Connection Issues](#common-connection-issues)
|
||||||
|
- [Error Messages](#error-messages)
|
||||||
|
6. [FAQ](#faq)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The Storage Provider feature allows you to securely store and manage credentials for various storage systems. Instead of entering connection details each time you create a transfer, you can now create reusable storage provider profiles. This approach offers several benefits:
|
||||||
|
|
||||||
|
- **Improved Security**: Credentials are stored securely using AES-256 encryption
|
||||||
|
- **Simplified Management**: Update credentials in one place instead of in each transfer
|
||||||
|
- **Easier Testing**: Test connections before creating transfers
|
||||||
|
- **Reusability**: Use the same provider for multiple transfers
|
||||||
|
|
||||||
|
## Managing Storage Providers
|
||||||
|
|
||||||
|
### Viewing Your Storage Providers
|
||||||
|
|
||||||
|
To view your storage providers:
|
||||||
|
|
||||||
|
1. Navigate to the **Storage Providers** section in the left sidebar
|
||||||
|
2. You'll see a list of all storage providers you have created
|
||||||
|
3. The list shows the provider name, type, and creation date
|
||||||
|
|
||||||
|
### Creating a New Storage Provider
|
||||||
|
|
||||||
|
To create a new storage provider:
|
||||||
|
|
||||||
|
1. From the Storage Providers page, click the **Add Provider** button
|
||||||
|
2. Enter a descriptive name for the provider
|
||||||
|
3. Select the provider type from the dropdown (SFTP, S3, OneDrive, etc.)
|
||||||
|
4. Fill in the required fields for the selected provider type
|
||||||
|
5. Click **Save** to create the provider or **Save & Test** to create and test the connection
|
||||||
|
|
||||||
|
#### Example: Creating an S3 Provider
|
||||||
|
|
||||||
|
1. Name: "Company AWS S3 Bucket"
|
||||||
|
2. Type: S3
|
||||||
|
3. Fill in the required fields:
|
||||||
|
- Access Key: Your AWS access key
|
||||||
|
- Secret Key: Your AWS secret key
|
||||||
|
- Region: e.g., us-west-2
|
||||||
|
- Bucket: Your bucket name
|
||||||
|
- Endpoint: Leave blank for AWS S3 or specify for S3-compatible services
|
||||||
|
4. Click **Save & Test**
|
||||||
|
|
||||||
|
### Editing Storage Providers
|
||||||
|
|
||||||
|
To edit an existing storage provider:
|
||||||
|
|
||||||
|
1. From the Storage Providers list, click the **Edit** button next to the provider
|
||||||
|
2. Update the fields as needed
|
||||||
|
3. For security reasons, sensitive fields (passwords, secret keys) appear empty
|
||||||
|
- Leave these fields empty to keep the existing values
|
||||||
|
- Enter new values only if you want to change them
|
||||||
|
4. Click **Save** to update the provider
|
||||||
|
|
||||||
|
### Testing Connections
|
||||||
|
|
||||||
|
Testing your storage provider connections ensures they're properly configured:
|
||||||
|
|
||||||
|
1. From the Storage Providers list, click the **Test** button next to the provider
|
||||||
|
2. Or when creating/editing a provider, use the **Save & Test** button
|
||||||
|
3. The system will attempt to connect using the provided credentials
|
||||||
|
4. You'll see a success message or an error with details about what went wrong
|
||||||
|
|
||||||
|
### Deleting Storage Providers
|
||||||
|
|
||||||
|
To delete a storage provider:
|
||||||
|
|
||||||
|
1. From the Storage Providers list, click the **Delete** button next to the provider
|
||||||
|
2. A confirmation dialog will appear
|
||||||
|
- If the provider is used in any transfers, you'll see a warning listing those transfers
|
||||||
|
- You cannot delete a provider that's in use without first updating those transfers
|
||||||
|
3. Confirm deletion if the provider is not in use
|
||||||
|
|
||||||
|
## Using Storage Providers in Transfers
|
||||||
|
|
||||||
|
### Creating Transfers with Storage Providers
|
||||||
|
|
||||||
|
To create a new transfer using storage providers:
|
||||||
|
|
||||||
|
1. Navigate to the **Transfers** section and click **New Transfer**
|
||||||
|
2. Fill in the transfer name and schedule as usual
|
||||||
|
3. In the Source section, select **Provider** and choose from the dropdown
|
||||||
|
- Only providers of appropriate types will be shown
|
||||||
|
- You'll see only providers you've created (unless you're an admin)
|
||||||
|
4. In the Destination section, also select a provider
|
||||||
|
5. Configure other transfer settings as needed (paths, file patterns, etc.)
|
||||||
|
6. Click **Save** to create the transfer
|
||||||
|
|
||||||
|
### Converting Existing Transfers
|
||||||
|
|
||||||
|
Existing transfers with embedded credentials can be converted to use storage providers:
|
||||||
|
|
||||||
|
1. Edit an existing transfer
|
||||||
|
2. In the Source section, click **Convert to Provider**
|
||||||
|
- This will create a new storage provider using the embedded credentials
|
||||||
|
- The provider will be named based on the transfer name
|
||||||
|
3. Do the same for the Destination section if needed
|
||||||
|
4. Click **Save** to update the transfer
|
||||||
|
|
||||||
|
## Provider Type Reference
|
||||||
|
|
||||||
|
### SFTP Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Host**: The hostname or IP address of the SFTP server
|
||||||
|
- **Port**: Server port (usually 22)
|
||||||
|
- **Username**: Your SFTP username
|
||||||
|
- **Authentication Method**: Password or Key File
|
||||||
|
- **Password**: Your SFTP password (if using password authentication)
|
||||||
|
- **Key File**: Path to SSH private key file (if using key authentication)
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Key File Password**: Password for the key file (if the key is password-protected)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Company SFTP Server
|
||||||
|
Type: SFTP
|
||||||
|
Host: sftp.example.com
|
||||||
|
Port: 22
|
||||||
|
Username: user123
|
||||||
|
Authentication: Password
|
||||||
|
Password: ********
|
||||||
|
```
|
||||||
|
|
||||||
|
### S3 Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Access Key**: Your S3 access key ID
|
||||||
|
- **Secret Key**: Your S3 secret access key
|
||||||
|
- **Bucket**: The S3 bucket name
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Region**: The AWS region (e.g., us-east-1)
|
||||||
|
- **Endpoint**: Server URL for S3-compatible services (leave blank for AWS S3)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Analytics Data Bucket
|
||||||
|
Type: S3
|
||||||
|
Access Key: AKIAIOSFODNN7EXAMPLE
|
||||||
|
Secret Key: ********
|
||||||
|
Region: us-west-2
|
||||||
|
Bucket: data-analytics-bucket
|
||||||
|
```
|
||||||
|
|
||||||
|
### OneDrive Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Client ID**: Your Microsoft application client ID
|
||||||
|
- **Client Secret**: Your Microsoft application client secret
|
||||||
|
- **Refresh Token**: OAuth refresh token for authentication
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Drive ID**: Specific drive ID (for accessing shared or team drives)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Marketing OneDrive
|
||||||
|
Type: OneDrive
|
||||||
|
Client ID: 12345678-1234-1234-1234-123456789012
|
||||||
|
Client Secret: ********
|
||||||
|
Refresh Token: ********
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Drive Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Client ID**: Your Google API client ID
|
||||||
|
- **Client Secret**: Your Google API client secret
|
||||||
|
- **Refresh Token**: OAuth refresh token for authentication
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Team Drive**: Team drive ID (for accessing shared drives)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Sales Team Drive
|
||||||
|
Type: Google Drive
|
||||||
|
Client ID: 123456789012-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com
|
||||||
|
Client Secret: ********
|
||||||
|
Refresh Token: ********
|
||||||
|
Team Drive: 0ABCDEFGhijklMNOPQrstuvwxyz
|
||||||
|
```
|
||||||
|
|
||||||
|
### FTP Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Host**: The hostname or IP address of the FTP server
|
||||||
|
- **Port**: Server port (usually 21)
|
||||||
|
- **Username**: Your FTP username
|
||||||
|
- **Password**: Your FTP password
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Passive Mode**: Enable/disable passive mode (default: enabled)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Legacy FTP Server
|
||||||
|
Type: FTP
|
||||||
|
Host: ftp.example.com
|
||||||
|
Port: 21
|
||||||
|
Username: ftpuser
|
||||||
|
Password: ********
|
||||||
|
Passive Mode: Enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### SMB Configuration
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
- **Host**: The hostname or IP address of the SMB/CIFS server
|
||||||
|
- **Share**: The share name
|
||||||
|
- **Username**: Your username
|
||||||
|
- **Password**: Your password
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
- **Domain**: Windows domain (if applicable)
|
||||||
|
- **Port**: Server port (default: 445)
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```
|
||||||
|
Name: Finance Share
|
||||||
|
Type: SMB
|
||||||
|
Host: fileserver.example.com
|
||||||
|
Share: finance
|
||||||
|
Username: jsmith
|
||||||
|
Password: ********
|
||||||
|
Domain: EXAMPLE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Connection Issues
|
||||||
|
|
||||||
|
#### SFTP Connection Problems
|
||||||
|
|
||||||
|
- **Authentication Failed**: Verify username and password/key file
|
||||||
|
- **Host Not Found**: Check hostname and network connectivity
|
||||||
|
- **Permission Denied**: Ensure the user has proper permissions on the server
|
||||||
|
- **Connection Timeout**: Check firewall settings and server availability
|
||||||
|
|
||||||
|
#### S3 Connection Problems
|
||||||
|
|
||||||
|
- **Access Denied**: Verify access key, secret key, and bucket permissions
|
||||||
|
- **Invalid Region**: Ensure the region matches the bucket's region
|
||||||
|
- **No Such Bucket**: Verify the bucket name and existence
|
||||||
|
- **Endpoint Error**: For S3-compatible services, verify the endpoint URL
|
||||||
|
|
||||||
|
#### OAuth Provider Issues (OneDrive/Google Drive)
|
||||||
|
|
||||||
|
- **Invalid Client**: Verify client ID and secret
|
||||||
|
- **Token Expired**: Refresh tokens may need to be regenerated
|
||||||
|
- **Permission Scope**: Ensure the token has appropriate scopes for file access
|
||||||
|
- **Rate Limiting**: You may be making too many requests in a short period
|
||||||
|
|
||||||
|
### Error Messages
|
||||||
|
|
||||||
|
Common error messages and their solutions:
|
||||||
|
|
||||||
|
| Error Message | Possible Cause | Solution |
|
||||||
|
|---------------|----------------|----------|
|
||||||
|
| "Connection refused" | Server is not running or blocked by firewall | Check server status and firewall settings |
|
||||||
|
| "Authentication failed" | Incorrect credentials | Verify username/password or key file |
|
||||||
|
| "Invalid access key" | Incorrect or expired AWS credentials | Check your access key ID and regenerate if needed |
|
||||||
|
| "Permission denied" | Insufficient permissions | Check file/folder permissions on the server |
|
||||||
|
| "Connection timed out" | Network issue or server unavailable | Check network connectivity and server status |
|
||||||
|
| "No such file or directory" | Path does not exist | Verify the path exists on the server |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: Can I use the same storage provider for multiple transfers?**
|
||||||
|
A: Yes, that's one of the main benefits. Create the provider once and use it in as many transfers as needed.
|
||||||
|
|
||||||
|
**Q: Can I see the passwords or secret keys I've stored?**
|
||||||
|
A: No, for security reasons, passwords and secret keys are never displayed after they're saved. You can update them, but you cannot view the existing values.
|
||||||
|
|
||||||
|
**Q: What happens if I need to update credentials?**
|
||||||
|
A: Edit the storage provider and enter the new credentials. All transfers using that provider will automatically use the updated credentials.
|
||||||
|
|
||||||
|
**Q: Are my credentials secure?**
|
||||||
|
A: Yes, all sensitive information is encrypted using AES-256 encryption before being stored in the database.
|
||||||
|
|
||||||
|
**Q: Can other users see my storage providers?**
|
||||||
|
A: No, each user can only see and use their own storage providers unless they have administrator privileges.
|
||||||
|
|
||||||
|
**Q: Can I export or import storage providers?**
|
||||||
|
A: Not currently. For security reasons, credential export is not supported.
|
||||||
|
|
||||||
|
**Q: What if I'm not sure if a provider is in use?**
|
||||||
|
A: When attempting to delete a provider, the system will show you all transfers that use it. You can also see usage information in the provider details.
|
||||||
|
|
||||||
|
**Q: Can I test a provider without creating a transfer?**
|
||||||
|
A: Yes, use the "Test" button on the provider list.
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import {themes as prismThemes} from 'prism-react-renderer';
|
||||||
|
import type {Config} from '@docusaurus/types';
|
||||||
|
import type * as Preset from '@docusaurus/preset-classic';
|
||||||
|
|
||||||
|
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
title: 'GoMFT',
|
||||||
|
tagline: 'A modern, web-based managed file transfer solution',
|
||||||
|
favicon: 'favicon.ico',
|
||||||
|
|
||||||
|
// Set the production url of your site here
|
||||||
|
url: 'https://starfleetcptn.github.io',
|
||||||
|
// Set the /<baseUrl>/ pathname under which your site is served
|
||||||
|
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||||
|
baseUrl: '/GoMFT/',
|
||||||
|
|
||||||
|
// GitHub pages deployment config.
|
||||||
|
// If you aren't using GitHub pages, you don't need these.
|
||||||
|
organizationName: 'StarFleetCPTN', // Usually your GitHub org/user name.
|
||||||
|
projectName: 'GoMFT', // Usually your repo name.
|
||||||
|
deploymentBranch: 'gh-pages',
|
||||||
|
trailingSlash: false,
|
||||||
|
|
||||||
|
// Explicit static directories configuration
|
||||||
|
staticDirectories: ['static'],
|
||||||
|
|
||||||
|
// Configure image loader to handle absolute paths with baseUrl
|
||||||
|
markdown: {
|
||||||
|
mermaid: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
onBrokenLinks: 'warn',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
||||||
|
// Even if you don't use internationalization, you can use this field to set
|
||||||
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
// may want to replace "en" with "zh-Hans".
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en'],
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'classic',
|
||||||
|
{
|
||||||
|
docs: {
|
||||||
|
sidebarPath: './sidebars.ts',
|
||||||
|
// Please change this to your repo.
|
||||||
|
editUrl:
|
||||||
|
'https://github.com/StarFleetCPTN/GoMFT/tree/main/docs',
|
||||||
|
},
|
||||||
|
blog: {
|
||||||
|
showReadingTime: true,
|
||||||
|
// Please change this to your repo.
|
||||||
|
editUrl:
|
||||||
|
'https://github.com/StarFleetCPTN/GoMFT/tree/main/docs',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
customCss: './src/css/custom.css',
|
||||||
|
},
|
||||||
|
} satisfies Preset.Options,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Add the local search plugin
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
require.resolve('@easyops-cn/docusaurus-search-local'),
|
||||||
|
{
|
||||||
|
// Whether to also index the docs/blog not written in the current language (false by default)
|
||||||
|
indexDocs: true,
|
||||||
|
indexBlog: true,
|
||||||
|
// Whether to also add the language name to the document ID, to differentiate documents with the same ID but different languages (false by default)
|
||||||
|
language: ['en'],
|
||||||
|
// Optional: path to a file containing a list of words to be highlighted in the search results (empty by default)
|
||||||
|
highlightSearchTermsOnTargetPage: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
// Replace with your project's social card
|
||||||
|
image: 'img/gomft-social-card.jpg',
|
||||||
|
navbar: {
|
||||||
|
title: 'GoMFT',
|
||||||
|
logo: {
|
||||||
|
alt: 'GoMFT Logo',
|
||||||
|
src: 'img/logo.svg',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'docSidebar',
|
||||||
|
sidebarId: 'docsSidebar',
|
||||||
|
position: 'left',
|
||||||
|
label: 'Documentation',
|
||||||
|
},
|
||||||
|
{to: '/docs/introduction/overview', label: 'Getting Started', position: 'left'},
|
||||||
|
{to: '/docs/development/contributing', label: 'Contributing', position: 'left'},
|
||||||
|
{
|
||||||
|
href: 'https://github.com/StarFleetCPTN/GoMFT',
|
||||||
|
label: 'GitHub',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://discord.gg/f9dwtM3j',
|
||||||
|
className: 'header-discord-link',
|
||||||
|
'aria-label': 'Discord community',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: 'dark',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Documentation',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Introduction',
|
||||||
|
to: '/docs/introduction/overview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Installation',
|
||||||
|
to: '/docs/getting-started/installation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Features',
|
||||||
|
to: '/docs/introduction/features',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Community',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
href: 'https://discord.gg/f9dwtM3j',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GitHub Discussions',
|
||||||
|
href: 'https://github.com/StarFleetCPTN/GoMFT/discussions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Issues',
|
||||||
|
href: 'https://github.com/StarFleetCPTN/GoMFT/issues',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'More',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/StarFleetCPTN/GoMFT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
copyright: `Copyright © ${new Date().getFullYear()} GoMFT. Built with Docusaurus.`,
|
||||||
|
},
|
||||||
|
prism: {
|
||||||
|
theme: prismThemes.github,
|
||||||
|
darkTheme: prismThemes.dracula,
|
||||||
|
},
|
||||||
|
} satisfies Preset.ThemeConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Fix image paths in Markdown files
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Function to recursively find all Markdown files
|
||||||
|
function findMarkdownFiles(directory) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
function traverse(dir) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
traverse(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(directory);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fix image paths in a file
|
||||||
|
function fixImagePaths(filePath) {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
let originalContent = content;
|
||||||
|
|
||||||
|
// Replace absolute paths with relative ones based on file location
|
||||||
|
const relativeToRoot = path.relative(path.dirname(filePath), path.resolve('static'));
|
||||||
|
const relativePath = relativeToRoot.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
// Replace any occurrence of  with 
|
||||||
|
content = content.replace(/!\[(.*?)\]\(\/img\/(.*?)\)/g,
|
||||||
|
(match, alt, imgPath) => ``);
|
||||||
|
|
||||||
|
// If content changed, write back to file
|
||||||
|
if (content !== originalContent) {
|
||||||
|
console.log(`Fixed image paths in ${filePath}`);
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
function main() {
|
||||||
|
console.log('Finding Markdown files...');
|
||||||
|
const docsDir = path.resolve('docs');
|
||||||
|
const mdFiles = findMarkdownFiles(docsDir);
|
||||||
|
|
||||||
|
console.log(`Found ${mdFiles.length} Markdown files`);
|
||||||
|
|
||||||
|
let fixedCount = 0;
|
||||||
|
for (const file of mdFiles) {
|
||||||
|
if (fixImagePaths(file)) {
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Fixed image paths in ${fixedCount} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "go-mft-docs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"start": "docusaurus start",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids",
|
||||||
|
"typecheck": "tsc",
|
||||||
|
"deploy-gh-pages": "GIT_USER=StarFleetCPTN USE_SSH=true yarn deploy",
|
||||||
|
"prepare-screenshots": "node -e \"const fs = require('fs'); const paths = ['./static/screenshots', './static/img/screenshots', './static/img']; paths.forEach(path => { if (!fs.existsSync(path)) { fs.mkdirSync(path, { recursive: true }); } }); fs.readdirSync('../screenshots').forEach(file => { paths.forEach(path => { const targetFile = path + '/' + file; if (!fs.existsSync(targetFile)) { fs.copyFileSync('../screenshots/' + file, targetFile); } }); });\"",
|
||||||
|
"fix-image-paths": "node fix-image-paths.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.7.0",
|
||||||
|
"@docusaurus/preset-classic": "3.7.0",
|
||||||
|
"@easyops-cn/docusaurus-search-local": "^0.49.2",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prism-react-renderer": "^2.3.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "3.7.0",
|
||||||
|
"@docusaurus/tsconfig": "3.7.0",
|
||||||
|
"@docusaurus/types": "3.7.0",
|
||||||
|
"typescript": "~5.6.2"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.5%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 3 chrome version",
|
||||||
|
"last 3 firefox version",
|
||||||
|
"last 5 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
|
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creating a sidebar enables you to:
|
||||||
|
- create an ordered group of docs
|
||||||
|
- render a sidebar for each doc of that group
|
||||||
|
- provide next/previous navigation
|
||||||
|
|
||||||
|
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||||
|
|
||||||
|
Create as many sidebars as you want.
|
||||||
|
*/
|
||||||
|
const sidebars: SidebarsConfig = {
|
||||||
|
docsSidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Introduction',
|
||||||
|
items: ['introduction/overview', 'introduction/features'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Getting Started',
|
||||||
|
items: ['getting-started/installation', 'getting-started/configuration', 'getting-started/quick-start', 'getting-started/docker', 'getting-started/traditional'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Core Concepts',
|
||||||
|
items: ['core-concepts/transfers', 'core-concepts/connections', 'core-concepts/schedules', 'core-concepts/monitoring'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'User Guides',
|
||||||
|
items: ['user-guides/storage-provider-guide'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Advanced Features',
|
||||||
|
items: [
|
||||||
|
'advanced/admin-tools',
|
||||||
|
'advanced/command-line-tool',
|
||||||
|
'advanced/notifications-overview',
|
||||||
|
'advanced/gotify-notifications',
|
||||||
|
'advanced/ntfy-notifications',
|
||||||
|
'advanced/pushbullet-notifications',
|
||||||
|
'advanced/pushover-notifications',
|
||||||
|
'advanced/webhook-notifications',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Security',
|
||||||
|
items: ['security/best-practices', 'security/authentication', 'security/non-root'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Development',
|
||||||
|
items: ['development/project-structure', 'development/contributing'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sidebars;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
type FeatureItem = {
|
||||||
|
title: string;
|
||||||
|
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||||
|
description: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeatureList: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
title: 'Easy to Use',
|
||||||
|
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Docusaurus was designed from the ground up to be easily installed and
|
||||||
|
used to get your website up and running quickly.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Focus on What Matters',
|
||||||
|
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Docusaurus lets you focus on your docs, and we'll do the chores. Go
|
||||||
|
ahead and move your docs into the <code>docs</code> directory.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Powered by React',
|
||||||
|
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Extend or customize your website layout by reusing React. Docusaurus can
|
||||||
|
be extended while reusing the same header and footer.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Feature({title, Svg, description}: FeatureItem) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('col col--4')}>
|
||||||
|
<div className="text--center">
|
||||||
|
<Svg className={styles.featureSvg} role="img" />
|
||||||
|
</div>
|
||||||
|
<div className="text--center padding-horiz--md">
|
||||||
|
<Heading as="h3">{title}</Heading>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomepageFeatures(): ReactNode {
|
||||||
|
return (
|
||||||
|
<section className={styles.features}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
{FeatureList.map((props, idx) => (
|
||||||
|
<Feature key={idx} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureSvg {
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Any CSS included here will be global. The classic template
|
||||||
|
* bundles Infima by default. Infima is a CSS framework designed to
|
||||||
|
* work well for content-centric websites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* You can override the default Infima variables here. */
|
||||||
|
:root {
|
||||||
|
--ifm-color-primary: #2e6db8;
|
||||||
|
--ifm-color-primary-dark: #2962a6;
|
||||||
|
--ifm-color-primary-darker: #275c9c;
|
||||||
|
--ifm-color-primary-darkest: #204c81;
|
||||||
|
--ifm-color-primary-light: #3378ca;
|
||||||
|
--ifm-color-primary-lighter: #3e7fcf;
|
||||||
|
--ifm-color-primary-lightest: #5a92d6;
|
||||||
|
--ifm-code-font-size: 95%;
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
/* Custom colors */
|
||||||
|
--ifm-color-secondary: #54b4d3;
|
||||||
|
--ifm-color-success: #28a745;
|
||||||
|
--ifm-color-info: #54b4d3;
|
||||||
|
--ifm-color-warning: #ffc107;
|
||||||
|
--ifm-color-danger: #dc3545;
|
||||||
|
|
||||||
|
/* Font settings */
|
||||||
|
--ifm-font-family-base: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif;
|
||||||
|
--ifm-heading-font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--ifm-color-primary: #4d94ff;
|
||||||
|
--ifm-color-primary-dark: #2d81ff;
|
||||||
|
--ifm-color-primary-darker: #1d77ff;
|
||||||
|
--ifm-color-primary-darkest: #0058dc;
|
||||||
|
--ifm-color-primary-light: #6da7ff;
|
||||||
|
--ifm-color-primary-lighter: #7db1ff;
|
||||||
|
--ifm-color-primary-lightest: #aeccff;
|
||||||
|
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
/* Custom colors in dark mode */
|
||||||
|
--ifm-background-color: #1a1a1a;
|
||||||
|
--ifm-background-surface-color: #242526;
|
||||||
|
--ifm-color-secondary: #4290ac;
|
||||||
|
--ifm-color-success: #2a9d47;
|
||||||
|
--ifm-color-info: #4290ac;
|
||||||
|
--ifm-color-warning: #d9a406;
|
||||||
|
--ifm-color-danger: #bd2130;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero--primary {
|
||||||
|
--ifm-hero-background-color: var(--ifm-color-primary);
|
||||||
|
--ifm-hero-text-color: var(--ifm-font-color-base-inverse);
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add custom button spacing on homepage */
|
||||||
|
.buttons {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo styling */
|
||||||
|
.navbar__logo {
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero logo styling */
|
||||||
|
.hero__logo {
|
||||||
|
width: 120px;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styling for feature sections */
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section styling */
|
||||||
|
.section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionAlt {
|
||||||
|
background-color: var(--ifm-color-emphasis-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer styling */
|
||||||
|
.footer {
|
||||||
|
padding: 2rem 0;
|
||||||
|
background-color: var(--ifm-color-primary-darkest);
|
||||||
|
color: var(--ifm-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--ifm-color-primary-lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
color: var(--ifm-color-white);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__title {
|
||||||
|
color: var(--ifm-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link-item {
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link-item:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .footer {
|
||||||
|
background-color: #191919;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation styling */
|
||||||
|
.markdown h1:first-child {
|
||||||
|
--ifm-h1-font-size: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h2 {
|
||||||
|
--ifm-h2-font-size: 2rem;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
border-bottom: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h3 {
|
||||||
|
--ifm-h3-font-size: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code block styling */
|
||||||
|
pre {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discord button styling */
|
||||||
|
.header-discord-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-discord-link::before {
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
background: url("../../static/img/discord.svg") no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .header-discord-link::before {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discord badge for other elements */
|
||||||
|
.discord-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #5865F2;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-badge:hover {
|
||||||
|
background-color: #4752c4;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-badge::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
background: url("../../static/img/discord.svg") no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
margin-right: 6px;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||||
|
* and scoped locally.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.heroBanner {
|
||||||
|
padding: 4rem 0;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 996px) {
|
||||||
|
.heroBanner {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionAlt {
|
||||||
|
background-color: var(--ifm-color-emphasis-100);
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import Layout from '@theme/Layout';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
|
||||||
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
function HomepageHeader() {
|
||||||
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col col--8 col--offset-2">
|
||||||
|
<img
|
||||||
|
className="hero__logo"
|
||||||
|
src="img/logo.svg"
|
||||||
|
alt="Project Logo"
|
||||||
|
/>
|
||||||
|
<Heading as="h1" className="hero__title">
|
||||||
|
{siteConfig.title}
|
||||||
|
</Heading>
|
||||||
|
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
className="button button--secondary button--lg"
|
||||||
|
to="/docs/introduction/overview">
|
||||||
|
Get Started
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="button button--outline button--lg button--secondary"
|
||||||
|
to="https://github.com/StarFleetCPTN/GoMFT">
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Easy to Use',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
GoMFT was designed from the ground up to be easily installed and
|
||||||
|
used to get your file transfers up and running quickly.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Multi-Protocol Support',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
GoMFT leverages the power of rclone to support over 40 storage
|
||||||
|
systems including S3, SFTP, Google Drive, and more.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Powerful Scheduling',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Schedule your file transfers using familiar cron syntax for recurring transfers,
|
||||||
|
or run them on-demand with the intuitive web interface.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeatureProps {
|
||||||
|
title: string;
|
||||||
|
description: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Feature({title, description}: FeatureProps) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('col col--4')}>
|
||||||
|
<div className="text--center padding-horiz--md">
|
||||||
|
<Heading as="h3">{title}</Heading>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home(): React.ReactNode {
|
||||||
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
title={`${siteConfig.title} - Modern Managed File Transfer Solution`}
|
||||||
|
description="GoMFT is a modern, open-source managed file transfer solution with multi-protocol support, scheduling capabilities, and a user-friendly web interface">
|
||||||
|
<HomepageHeader />
|
||||||
|
<main>
|
||||||
|
<section className={styles.features}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
{FeatureList().map((props, idx) => (
|
||||||
|
<Feature key={idx} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={clsx(styles.section, styles.sectionAlt)}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col col--6">
|
||||||
|
<Heading as="h2">Modern Web Interface</Heading>
|
||||||
|
<p>
|
||||||
|
GoMFT provides a clean, responsive web interface for managing your file transfers.
|
||||||
|
The dashboard gives you at-a-glance information about transfer status, recent jobs,
|
||||||
|
and system health.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
className="button button--primary"
|
||||||
|
to="/docs/core-concepts/monitoring">
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="col col--6">
|
||||||
|
<img src="img/dashboard.gomft.png" alt="GoMFT Dashboard" className="shadow--md" style={{borderRadius: '8px'}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col col--6">
|
||||||
|
<img src="img/transfer.config.gomft.png" alt="Transfer Configuration" className="shadow--md" style={{borderRadius: '8px'}} />
|
||||||
|
</div>
|
||||||
|
<div className="col col--6">
|
||||||
|
<Heading as="h2">Easy Deployment</Heading>
|
||||||
|
<p>
|
||||||
|
Deploy GoMFT quickly using Docker, or install it directly on your system.
|
||||||
|
The application is lightweight and can run on various platforms including
|
||||||
|
Linux, macOS, and Windows.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
className="button button--primary"
|
||||||
|
to="/docs/getting-started/installation">
|
||||||
|
Installation Guide
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={clsx(styles.section, styles.sectionAlt)}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="text--center">
|
||||||
|
<Heading as="h2">Ready to Get Started?</Heading>
|
||||||
|
<p>
|
||||||
|
Check out our documentation to learn how to set up and use GoMFT for your file transfer needs.
|
||||||
|
</p>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Link
|
||||||
|
className="button button--primary button--lg"
|
||||||
|
to="/docs/introduction/overview">
|
||||||
|
Read the Docs
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="button button--secondary button--lg"
|
||||||
|
to="https://github.com/StarFleetCPTN/GoMFT">
|
||||||
|
GitHub Repository
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Markdown page example
|
||||||
|
---
|
||||||
|
|
||||||
|
# Markdown page example
|
||||||
|
|
||||||
|
You don't need React to write simple standalone pages.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import SearchBar from '@theme-original/SearchBar';
|
||||||
|
import type SearchBarType from '@theme/SearchBar';
|
||||||
|
import type {WrapperProps} from '@docusaurus/types';
|
||||||
|
|
||||||
|
type Props = WrapperProps<typeof SearchBarType>;
|
||||||
|
|
||||||
|
export default function SearchBarWrapper(props: Props): ReactNode {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchBar {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 350 B |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
# Images directory
|
||||||
|
|
||||||
|
This directory contains various images used throughout the documentation, including:
|
||||||
|
|
||||||
|
1. Logo files
|
||||||
|
2. Screenshots
|
||||||
|
3. Icons and other assets
|
||||||
|
|
||||||
|
Some screenshots are automatically copied from the main repository's `/screenshots` directory during the build process.
|
||||||
|
|
||||||
|
The automated copy is handled by the `prepare-screenshots` script in package.json.
|
||||||
|
|
||||||
|
## Required Images
|
||||||
|
|
||||||
|
The following images are used in the documentation:
|
||||||
|
|
||||||
|
- `logo.svg` - The GoMFT logo for the header
|
||||||
|
- `favicon.ico` - The website favicon
|
||||||
|
- `docusaurus-social-card.jpg` - Social media preview image
|
||||||
|
- `dashboard.gomft.png` - Screenshot of the GoMFT dashboard
|
||||||
|
- `dashboard.dark.gomft.png` - Screenshot of the GoMFT dashboard in dark mode
|
||||||
|
- `transfer.config.gomft.png` - Screenshot of the transfer configuration
|
||||||
|
- `scheduled.jobs.gomft.png` - Screenshot of the scheduled jobs
|
||||||
|
- `transfer.history.gomft.png` - Screenshot of the transfer history
|
||||||
|
- `user.management.gomft.png` - Screenshot of the user management
|
||||||
|
- `role.management.gomft.png` - Screenshot of the role management
|
||||||
|
- `authentication.providers.gomft.png` - Screenshot of the authentication providers
|
||||||
|
- `notifications.gomft.png` - Screenshot of the notifications
|
||||||
|
- `notification.service.gomft.png` - Screenshot of the notification service
|
||||||
|
- `audit.logs.gomft.png` - Screenshot of the audit logs
|
||||||
|
- `file.details.gomft.png` - Screenshot of the file details
|
||||||
|
- `file.metadata.gomft.png` - Screenshot of the file metadata
|
||||||
|
- `database.tools.gomft.png` - Screenshot of the database tools
|
||||||
|
- `job.run.details.gomft.png` - Screenshot of the job run details
|
||||||
|
|
||||||
|
## Symlinks for Backward Compatibility
|
||||||
|
|
||||||
|
- `dashboard.png` → `dashboard.gomft.png`
|
||||||
|
- `create-transfer.png` → `transfer.config.gomft.png`
|
||||||
|
- `docker.png` - Placeholder for Docker-related screenshots
|
||||||
|
|
||||||
|
## Adding New Images
|
||||||
|
|
||||||
|
When adding images to this directory:
|
||||||
|
|
||||||
|
1. Use descriptive filenames
|
||||||
|
2. Optimize image size when possible
|
||||||
|
3. Use PNG for screenshots and SVG for vector graphics
|
||||||
|
4. Include alt text when referencing images in documentation
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 480 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" fill="#5865F2">
|
||||||
|
<path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 350 B |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 338 KiB |