├── .air.toml
├── .github
└── workflows
│ ├── docker-publish-dockerhub.yml
│ ├── docker-publish.yml
│ ├── docusaurus-deploy.yml
│ └── go-release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── build.js
├── components
├── admin.templ
├── admin_audit.templ
├── admin_database.templ
├── admin_logs.templ
├── admin_role_form.templ
├── admin_roles.templ
├── auth_provider_form.templ
├── auth_providers.templ
├── auth_providers_buttons.templ
├── components.go
├── config_form.templ
├── configs.templ
├── dashboard.templ
├── dashboard_notifications.templ
├── dashboard_notifications_page.templ
├── error.templ
├── file_metadata.templ
├── file_metadata
│ ├── details
│ │ └── file_metadata_details.templ
│ ├── dialog
│ │ └── file_metadata_dialog.templ
│ ├── list
│ │ ├── file_metadata_list.templ
│ │ └── file_metadata_list_partial.templ
│ ├── search
│ │ ├── file_metadata_search.templ
│ │ └── file_metadata_search_content.templ
│ ├── types.go
│ └── utils
│ │ ├── file_metadata_js.templ
│ │ └── file_metadata_utils.go
├── forgot_password.templ
├── history.templ
├── home.templ
├── job_form.templ
├── job_run_details.templ
├── jobs.templ
├── layout.templ
├── login.templ
├── notifications
│ ├── dialog
│ │ ├── dialog.templ
│ │ └── dialog_js.templ
│ ├── form
│ │ ├── fields
│ │ │ ├── email.templ
│ │ │ ├── gotify.templ
│ │ │ ├── ntfy.templ
│ │ │ ├── pushbullet.templ
│ │ │ ├── pushover.templ
│ │ │ └── webhook.templ
│ │ ├── form.templ
│ │ ├── form_js.templ
│ │ └── utils
│ │ │ └── utils.go
│ ├── list
│ │ ├── list.templ
│ │ └── list_js.templ
│ └── types
│ │ └── types.go
├── profile.templ
├── providers
│ ├── common
│ │ └── common.templ
│ ├── destination
│ │ ├── b2.templ
│ │ ├── destination.go
│ │ ├── ftp.templ
│ │ ├── gdrive.templ
│ │ ├── gphotos.templ
│ │ ├── hetzner.templ
│ │ ├── local.templ
│ │ ├── minio.templ
│ │ ├── nextcloud.templ
│ │ ├── s3.templ
│ │ ├── sftp.templ
│ │ ├── smb.templ
│ │ ├── wasabi.templ
│ │ └── webdav.templ
│ ├── provider_form.templ
│ ├── providers.go
│ └── source
│ │ ├── b2.templ
│ │ ├── ftp.templ
│ │ ├── gdrive.templ
│ │ ├── gphotos.templ
│ │ ├── hetzner.templ
│ │ ├── local.templ
│ │ ├── minio.templ
│ │ ├── nextcloud.templ
│ │ ├── s3.templ
│ │ ├── sftp.templ
│ │ ├── smb.templ
│ │ ├── source.go
│ │ ├── wasabi.templ
│ │ └── webdav.templ
├── register.templ
├── settings.templ
├── shared
│ └── toast
│ │ ├── toast.templ
│ │ └── toast_js.templ
├── test_result.templ
├── two_factor_backup_codes.templ
├── two_factor_backup_verify.templ
├── two_factor_setup.templ
├── two_factor_verify.templ
├── user_edit.templ
├── user_management.templ
└── users.templ
├── docker-compose.yaml
├── docs
├── .gitignore
├── README.md
├── docs
│ ├── advanced
│ │ ├── admin-tools.md
│ │ ├── gotify-notifications.md
│ │ ├── notifications-overview.md
│ │ ├── ntfy-notifications.md
│ │ ├── pushbullet-notifications.md
│ │ ├── pushover-notifications.md
│ │ └── webhook-notifications.md
│ ├── core-concepts
│ │ ├── connections.md
│ │ ├── monitoring.md
│ │ ├── schedules.md
│ │ └── transfers.md
│ ├── development
│ │ ├── code-review.md
│ │ ├── contributing.md
│ │ ├── project-structure.md
│ │ └── release-process.md
│ ├── getting-started
│ │ ├── configuration.md
│ │ ├── docker.md
│ │ ├── installation.md
│ │ ├── quick-start.md
│ │ └── traditional.md
│ ├── introduction
│ │ ├── features.md
│ │ └── overview.md
│ └── security
│ │ ├── authentication.md
│ │ ├── best-practices.md
│ │ └── non-root.md
├── docusaurus.config.ts
├── fix-image-paths.js
├── package-lock.json
├── package.json
├── sidebars.ts
├── src
│ ├── components
│ │ └── HomepageFeatures
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ ├── css
│ │ └── custom.css
│ ├── pages
│ │ ├── index.module.css
│ │ ├── index.tsx
│ │ └── markdown-page.md
│ └── theme
│ │ └── SearchBar.tsx
├── static
│ ├── .nojekyll
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── css
│ │ └── styles.dfe17619.css
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── images
│ │ ├── dashboard.gomft-038de3445e0d9228c5621ea0b5577c42.png
│ │ └── transfer.config.gomft-7583731408b086ca9c7bf3127aca9952.png
│ ├── img
│ │ ├── README.md
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── audit.logs.gomft.png
│ │ ├── authentication.providers.gomft.png
│ │ ├── create-transfer.png
│ │ ├── dashboard.dark.gomft.png
│ │ ├── dashboard.gomft.png
│ │ ├── dashboard.png
│ │ ├── database.tools.gomft.png
│ │ ├── discord.svg
│ │ ├── docker.png
│ │ ├── docusaurus-social-card.jpg
│ │ ├── docusaurus.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── file.details.gomft.png
│ │ ├── file.metadata.gomft.png
│ │ ├── gomft-social-card.jpg
│ │ ├── job.run.details.gomft.png
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ ├── notification.service.gomft.png
│ │ ├── notifications.gomft.png
│ │ ├── role.management.gomft.png
│ │ ├── scheduled.jobs.gomft.png
│ │ ├── site.webmanifest
│ │ ├── transfer.config.gomft.png
│ │ ├── transfer.history.gomft.png
│ │ ├── undraw_docusaurus_mountain.svg
│ │ ├── undraw_docusaurus_react.svg
│ │ ├── undraw_docusaurus_tree.svg
│ │ └── user.management.gomft.png
│ ├── screenshots
│ │ └── README.md
│ └── site.webmanifest
└── tsconfig.json
├── entrypoint.sh
├── go.mod
├── go.sum
├── internal
├── api
│ └── api.go
├── auth
│ ├── jwt.go
│ ├── password.go
│ └── totp.go
├── config
│ └── config.go
├── db
│ ├── audit_log.go
│ ├── auth_provider.go
│ ├── auth_provider_store.go
│ ├── db.go
│ ├── file_metadata.go
│ ├── file_metadata_store.go
│ ├── job.go
│ ├── job_store.go
│ ├── migrations
│ │ ├── 001_initial_schema.go
│ │ ├── 002_update_gdrive_type.go
│ │ ├── 003_add_2fa.go
│ │ ├── 004_add_audit_logs.go
│ │ ├── 005_add_default_roles.go
│ │ ├── 006_add_timestamps_to_job_histories.go
│ │ ├── 007_add_notification_services.go
│ │ ├── 008_add_user_notifications.go
│ │ ├── 009_add_rclone_tables.go
│ │ ├── 010_add_rclone_command_to_config.go
│ │ ├── 011_add_auth_providers.go
│ │ ├── 012_alter_boolean_defaults.go
│ │ ├── 012a_recover_transfer_configs_rename.go
│ │ ├── 012b_recover_notification_services_rename.go
│ │ ├── 012c_recover_auth_providers_rename.go
│ │ ├── 013_cleanup_invalid_booleans.go
│ │ └── migrations.go
│ ├── notification.go
│ ├── rclone.go
│ ├── rclone_store.go
│ ├── role.go
│ ├── role_store.go
│ ├── transfer_config.go
│ ├── transfer_config_store.go
│ ├── user.go
│ ├── user_notification.go
│ └── user_store.go
├── email
│ ├── email.go
│ ├── email_test.go
│ └── mock_email.go
├── logging
│ └── logger.go
├── rclone_service
│ ├── rclone_service.go
│ └── rclone_service_test.go
├── scheduler
│ ├── job_executor.go
│ ├── job_executor_test.go
│ ├── logger.go
│ ├── logger_test.go
│ ├── metadata.go
│ ├── metadata_test.go
│ ├── mock_scheduler.go
│ ├── notification.go
│ ├── notification_test.go
│ ├── scheduler.go
│ ├── scheduler_interface.go
│ ├── scheduler_test.go
│ ├── transfer_executor.go
│ ├── transfer_executor_test.go
│ ├── utils.go
│ └── utils_test.go
├── testutils
│ └── testutils.go
└── web
│ ├── handlers.go
│ └── handlers
│ ├── admin_handlers.go
│ ├── api_handlers.go
│ ├── auth_handlers.go
│ ├── auth_provider_handlers.go
│ ├── basic_handlers.go
│ ├── config_handlers.go
│ ├── dashboard_handlers.go
│ ├── database_handlers.go
│ ├── error_handlers.go
│ ├── file_metadata_handlers.go
│ ├── gdrive_handlers.go
│ ├── handler.go
│ ├── job_handlers.go
│ ├── notifications_handlers.go
│ ├── oauth_handlers.go
│ ├── path_handlers.go
│ ├── profile_handlers.go
│ ├── rclone_handlers.go
│ ├── routes.go
│ ├── settings_handlers.go
│ ├── two_factor_handlers.go
│ └── user_handlers.go
├── main.go
├── package-lock.json
├── package.json
├── screenshots
├── audit.logs.gomft.png
├── authentication.providers.gomft.png
├── dashboard.dark.gomft.png
├── dashboard.gomft.png
├── database.tools.gomft.png
├── file.details.gomft.png
├── file.metadata.gomft.png
├── job.run.details.gomft.png
├── notification.service.gomft.png
├── notifications.gomft.png
├── role.management.gomft.png
├── scheduled.jobs.gomft.png
├── transfer.config.gomft.png
├── transfer.history.gomft.png
└── user.management.gomft.png
├── static
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── css
│ └── app.css
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── img
│ ├── authelia.svg
│ ├── authentik.svg
│ ├── keycloak.svg
│ ├── logo.png
│ ├── logo.svg
│ ├── oauth2.svg
│ ├── oidc.svg
│ ├── pocket-id.svg
│ └── saml.svg
├── js
│ ├── app.js
│ ├── init.js
│ └── vendor.js
└── site.webmanifest
├── tailwind.config.js
└── testing.md
/.air.toml:
--------------------------------------------------------------------------------
1 | root = "."
2 | testdata_dir = "testdata"
3 | tmp_dir = "tmp"
4 |
5 | [build]
6 | args_bin = []
7 | bin = "./tmp/main"
8 | cmd = "templ generate && go build -o ./tmp/main ."
9 | delay = 1000
10 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
11 | exclude_file = []
12 | exclude_regex = ["_test.go", "_templ.go"]
13 | exclude_unchanged = false
14 | follow_symlink = false
15 | full_bin = ""
16 | include_dir = []
17 | include_ext = ["go", "tpl", "tmpl", "templ", "html"]
18 | kill_delay = "0s"
19 | log = "build-errors.log"
20 | send_interrupt = false
21 | stop_on_error = true
22 |
23 | [color]
24 | app = ""
25 | build = "yellow"
26 | main = "magenta"
27 | runner = "green"
28 | watcher = "cyan"
29 |
30 | [log]
31 | time = false
32 |
33 | [misc]
34 | clean_on_exit = false
35 |
36 | [screen]
37 | clear_on_rebuild = false
38 | keep_scroll = true
--------------------------------------------------------------------------------
/.github/workflows/docker-publish-dockerhub.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish Docker Image DockHub
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 | workflow_dispatch:
8 | inputs:
9 | manual_version:
10 | description: 'Manual version override (leave empty to use git tag)'
11 | required: false
12 | default: ''
13 |
14 | env:
15 | # Use github.repository as the default image name
16 | IMAGE_NAME: ${{ github.repository }}
17 |
18 | jobs:
19 | build-and-push:
20 | runs-on: ubuntu-latest
21 | # Set the permissions needed for the DockerHub token to push to GHCR
22 | permissions:
23 | contents: read
24 | packages: write
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v4
29 | with:
30 | fetch-depth: 0 # Needed to get all tags for versioning
31 |
32 | # Set up Node.js for frontend build
33 | - name: Set up Node.js
34 | uses: actions/setup-node@v4
35 | with:
36 | node-version: '20'
37 | cache: 'npm'
38 |
39 | # Install dependencies
40 | - name: Install dependencies
41 | run: npm ci
42 |
43 | # Build frontend assets
44 | - name: Build frontend assets
45 | run: |
46 | node build.js
47 | ls -la static/dist/
48 |
49 | # Set version information
50 | - name: Set Version
51 | id: version
52 | run: |
53 | if [[ "${{ github.event.inputs.manual_version }}" != "" ]]; then
54 | echo "VERSION=${{ github.event.inputs.manual_version }}" >> $GITHUB_ENV
55 | elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
56 | VERSION=${GITHUB_REF#refs/tags/}
57 | echo "VERSION=$VERSION" >> $GITHUB_ENV
58 | else
59 | VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")-$(git rev-parse --short HEAD)
60 | echo "VERSION=$VERSION" >> $GITHUB_ENV
61 | fi
62 | echo "BUILD_TIME=$(date -u +'%Y-%m-%d_%H:%M:%S')" >> $GITHUB_ENV
63 | echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
64 |
65 | # Set up Docker Buildx for efficient builds
66 | - name: Set up Docker Buildx
67 | uses: docker/setup-buildx-action@v3
68 |
69 | # Login to DockerHub Container Registry
70 | - name: Log in to DockerHub Container Registry
71 | uses: docker/login-action@v3
72 | with:
73 | username: ${{ secrets.DOCKERHUB_USERNAME }}
74 | password: ${{ secrets.DOCKERHUB_TOKEN }}
75 |
76 |
77 | # Extract metadata for Docker image
78 | - name: Extract Docker metadata
79 | id: meta
80 | uses: docker/metadata-action@v5
81 | with:
82 | images: starfleetcptn/gomft
83 | tags: |
84 | type=semver,pattern={{version}}
85 | type=semver,pattern={{major}}.{{minor}}
86 | type=ref,event=branch
87 | type=ref,event=pr
88 | type=sha,format=long
89 | type=raw,value=latest,enable={{is_default_branch}}
90 |
91 | # Build and push Docker image
92 | - name: Build and push
93 | uses: docker/build-push-action@v5
94 | with:
95 | context: .
96 | push: ${{ github.event_name != 'pull_request' }}
97 | tags: ${{ steps.meta.outputs.tags }}
98 | labels: ${{ steps.meta.outputs.labels }}
99 | platforms: linux/amd64,linux/arm64,linux/arm/v7
100 | build-args: |
101 | VERSION=${{ env.VERSION }}
102 | BUILD_TIME=${{ env.BUILD_TIME }}
103 | COMMIT=${{ env.COMMIT }}
104 | UID=1000
105 | GID=1000
106 | cache-from: type=gha
107 | cache-to: type=gha,mode=max
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish Docker Image
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 | workflow_dispatch:
8 | inputs:
9 | manual_version:
10 | description: 'Manual version override (leave empty to use git tag)'
11 | required: false
12 | default: ''
13 |
14 | env:
15 | # Use github.repository as the default image name
16 | IMAGE_NAME: ${{ github.repository }}
17 | REGISTRY: ghcr.io
18 |
19 | jobs:
20 | build-and-push:
21 | runs-on: ubuntu-latest
22 | # Set the permissions needed for the GitHub token to push to GHCR
23 | permissions:
24 | contents: read
25 | packages: write
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 | with:
31 | fetch-depth: 0 # Needed to get all tags for versioning
32 |
33 | # Set up Node.js for frontend build
34 | - name: Set up Node.js
35 | uses: actions/setup-node@v4
36 | with:
37 | node-version: '20'
38 | cache: 'npm'
39 |
40 | # Install dependencies
41 | - name: Install dependencies
42 | run: npm ci
43 |
44 | # Build frontend assets
45 | - name: Build frontend assets
46 | run: |
47 | node build.js
48 | ls -la static/dist/
49 |
50 | # Set version information
51 | - name: Set Version
52 | id: version
53 | run: |
54 | if [[ "${{ github.event.inputs.manual_version }}" != "" ]]; then
55 | echo "VERSION=${{ github.event.inputs.manual_version }}" >> $GITHUB_ENV
56 | elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
57 | VERSION=${GITHUB_REF#refs/tags/}
58 | echo "VERSION=$VERSION" >> $GITHUB_ENV
59 | else
60 | VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")-$(git rev-parse --short HEAD)
61 | echo "VERSION=$VERSION" >> $GITHUB_ENV
62 | fi
63 | echo "BUILD_TIME=$(date -u +'%Y-%m-%d_%H:%M:%S')" >> $GITHUB_ENV
64 | echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
65 |
66 | # Set up Docker Buildx for efficient builds
67 | - name: Set up Docker Buildx
68 | uses: docker/setup-buildx-action@v3
69 |
70 | # Login to GitHub Container Registry
71 | - name: Log in to GitHub Container Registry
72 | uses: docker/login-action@v3
73 | with:
74 | registry: ${{ env.REGISTRY }}
75 | username: ${{ github.actor }}
76 | password: ${{ secrets.GITHUB_TOKEN }}
77 |
78 | # Extract metadata for Docker image
79 | - name: Extract Docker metadata
80 | id: meta
81 | uses: docker/metadata-action@v5
82 | with:
83 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
84 | tags: |
85 | type=semver,pattern={{version}}
86 | type=semver,pattern={{major}}.{{minor}}
87 | type=ref,event=branch
88 | type=ref,event=pr
89 | type=sha,format=long
90 | type=raw,value=latest,enable={{is_default_branch}}
91 |
92 | # Build and push Docker image
93 | - name: Build and push
94 | uses: docker/build-push-action@v5
95 | with:
96 | context: .
97 | push: ${{ github.event_name != 'pull_request' }}
98 | tags: ${{ steps.meta.outputs.tags }}
99 | labels: ${{ steps.meta.outputs.labels }}
100 | platforms: linux/amd64,linux/arm64,linux/arm/v7
101 | build-args: |
102 | VERSION=${{ env.VERSION }}
103 | BUILD_TIME=${{ env.BUILD_TIME }}
104 | COMMIT=${{ env.COMMIT }}
105 | UID=1000
106 | GID=1000
107 | cache-from: type=gha
108 | cache-to: type=gha,mode=max
--------------------------------------------------------------------------------
/.github/workflows/docusaurus-deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docusaurus to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths: ['docs/**']
7 | # Allows you to run this workflow manually from the Actions tab
8 | workflow_dispatch:
9 |
10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
11 | permissions:
12 | contents: read
13 | pages: write
14 | id-token: write
15 |
16 | # Allow only one concurrent deployment
17 | concurrency:
18 | group: "pages"
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | build:
23 | name: Deploy Docusaurus
24 | runs-on: ubuntu-latest
25 | defaults:
26 | run:
27 | working-directory: ./docs
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v4
31 |
32 | - name: Setup Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version: 18
36 | cache: npm
37 | cache-dependency-path: ./docs/package-lock.json
38 |
39 | - name: Install dependencies
40 | run: npm ci
41 |
42 | - name: Copy screenshots to static directory
43 | run: |
44 | npm run prepare-screenshots
45 | # Ensure the images referenced in the docs are definitely available
46 | mkdir -p static/img
47 | cp -v ../screenshots/dashboard.gomft.png static/img/
48 | cp -v ../screenshots/transfer.config.gomft.png static/img/
49 | ls -la static/img/dashboard.gomft.png static/img/transfer.config.gomft.png
50 |
51 | - name: Fix Markdown image paths
52 | run: npm run fix-image-paths
53 |
54 | - name: Verify screenshots exist
55 | run: |
56 | echo "Checking if screenshots were copied correctly..."
57 | if [ -f "static/img/dashboard.gomft.png" ]; then
58 | echo "✅ Found dashboard.gomft.png in static/img/"
59 | else
60 | echo "❌ Missing dashboard.gomft.png in static/img/"
61 | exit 1
62 | fi
63 | if [ -f "static/img/transfer.config.gomft.png" ]; then
64 | echo "✅ Found transfer.config.gomft.png in static/img/"
65 | else
66 | echo "❌ Missing transfer.config.gomft.png in static/img/"
67 | exit 1
68 | fi
69 |
70 | - name: Build
71 | run: npm run build
72 |
73 | - name: Setup Pages
74 | uses: actions/configure-pages@v4
75 |
76 | - name: Upload artifact
77 | uses: actions/upload-pages-artifact@v3
78 | with:
79 | path: ./docs/build
80 |
81 | - name: Deploy to GitHub Pages
82 | id: deployment
83 | uses: actions/deploy-pages@v4
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build output
8 | *.test
9 | *.out
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.cov
13 |
14 | # Temporary files
15 | *.tmp
16 | *.temp
17 |
18 | # Build directories
19 | _build/
20 | build/
21 |
22 | # Vendor directory
23 | /vendor/
24 |
25 | # Go workspace file
26 | go.work
27 | go.work.sum
28 |
29 | # IDE/editor specific files
30 | .idea/
31 | .vscode/
32 | *.swp
33 | *~
34 |
35 | # Logs
36 | *.log
37 |
38 | # Dependency directories
39 | node_modules/
40 |
41 | # OS generated files
42 | .DS_Store
43 | Thumbs.db
44 |
45 | # Ignore all Go files in the components directory
46 | *_templ.go
47 |
48 | # Ignore the data directory
49 | data/
50 |
51 | # Ignore .env files
52 | .env
53 | *.env
54 |
55 | # Ignore the tmp directory
56 | tmp/
57 |
58 | # Ignore the configs directory
59 | configs/
60 |
61 | # Ignore the backups directory
62 | backups/
63 |
64 | # Ignore Dirs
65 | /source/
66 | /destination/
67 | /archive/
68 |
69 | # Ignore the dist directory
70 | static/dist/
71 |
72 | # Ignore binaries
73 | gomft
74 |
75 | # Docusaurus files
76 | docs/.docusaurus/
77 | docs/.cache-loader/
78 | docs/build/
79 | docs/build-searchindex/
80 | docs/static/search/
81 | docs/static/js/
82 | docs/node_modules/
83 | docs/.env*
84 | docs/*.log
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build frontend assets
2 | FROM node:20-alpine AS frontend-builder
3 |
4 | WORKDIR /app
5 |
6 | # Copy all files needed for the build first
7 | COPY package.json package-lock.json ./
8 | COPY build.js ./
9 | COPY static/ ./static/
10 | COPY tailwind.config.js ./
11 |
12 | # Debug: Show the contents of build.js
13 | RUN echo "Contents of build.js:" && cat build.js
14 |
15 | # Remove the build script from postinstall
16 | RUN sed -i 's/"postinstall": "npm run build",//' package.json
17 |
18 | # Install dependencies and build with verbose output
19 | RUN npm ci && \
20 | echo "Building frontend assets..." && \
21 | node build.js && \
22 | echo "Build complete. Contents of dist:" && \
23 | ls -la static/dist/ && \
24 | echo "Sample of app.js:" && \
25 | head -n 10 static/dist/app.js && \
26 | echo "Sample of app.css:" && \
27 | head -n 10 static/dist/app.css
28 |
29 | # Go build stage
30 | FROM golang:1.24-alpine AS builder
31 |
32 | WORKDIR /app
33 |
34 | # Accept build arguments for version information
35 | ARG VERSION=dev
36 | ARG BUILD_TIME=unknown
37 | ARG COMMIT=unknown
38 | # Architecture-related build arguments
39 | ARG TARGETOS=linux
40 | ARG TARGETARCH=amd64
41 | ARG TARGETVARIANT=""
42 |
43 | # Install build dependencies
44 | RUN apk add --no-cache git build-base
45 |
46 | # Install templ compiler
47 | RUN go install github.com/a-h/templ/cmd/templ@latest
48 |
49 | # Copy go module files first for better layer caching
50 | COPY go.mod go.sum ./
51 | RUN go mod download
52 |
53 | # Create static directory structure
54 | RUN mkdir -p /app/static/dist
55 |
56 | # Copy built frontend assets from frontend-builder BEFORE copying Go source
57 | COPY --from=frontend-builder /app/static/dist/ /app/static/dist/
58 |
59 | # Copy the rest of the static files
60 | COPY static/ /app/static/
61 |
62 | # Verify static files are in place before Go build
63 | RUN echo "Verifying static files before Go build:" && \
64 | ls -la /app/static/dist/
65 |
66 | # Now copy the rest of the source code
67 | COPY . .
68 |
69 | # Generate template files from .templ files
70 | RUN templ generate
71 |
72 | # Compile the application with version information
73 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
74 | -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}" \
75 | -o /app/gomft
76 |
77 | # Install rclone with appropriate architecture
78 | RUN apk add --no-cache curl unzip && \
79 | if [ "$TARGETARCH" = "arm64" ]; then \
80 | RCLONE_ARCH="arm64"; \
81 | elif [ "$TARGETARCH" = "arm" ]; then \
82 | RCLONE_ARCH="arm-v7"; \
83 | else \
84 | RCLONE_ARCH="amd64"; \
85 | fi && \
86 | curl -O https://downloads.rclone.org/rclone-current-linux-${RCLONE_ARCH}.zip && \
87 | unzip rclone-current-linux-${RCLONE_ARCH}.zip && \
88 | cd rclone-*-linux-${RCLONE_ARCH} && \
89 | cp rclone /usr/local/bin/ && \
90 | chmod 755 /usr/local/bin/rclone && \
91 | cd .. && \
92 | rm -rf rclone*
93 |
94 | # Create a smaller runtime image
95 | FROM alpine:3.19
96 |
97 | # Add arguments for UID and GID with defaults
98 | ARG UID=1000
99 | ARG GID=1000
100 | ARG USERNAME=gomft
101 | ARG TARGETOS
102 | ARG TARGETARCH
103 |
104 | WORKDIR /app
105 |
106 | # Install runtime dependencies
107 | RUN apk add --no-cache ca-certificates tzdata sqlite bash shadow su-exec \
108 | && apk add --no-cache --virtual .user-deps \
109 | shadow curl xz
110 |
111 | # Create user and group with specified IDs
112 | RUN addgroup -g ${GID} ${USERNAME} && \
113 | adduser -D -u ${UID} -G ${USERNAME} -s /bin/sh ${USERNAME}
114 |
115 | # Copy the binary from the builder stage
116 | COPY --from=builder /app/gomft /app/
117 | COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
118 |
119 | # Copy components
120 | COPY components/ /app/components/
121 |
122 | # Copy entrypoint script
123 | COPY entrypoint.sh /entrypoint.sh
124 | RUN chmod +x /entrypoint.sh
125 |
126 | # Create data and backup directories
127 | RUN mkdir -p /app/data /app/backups
128 |
129 | # Create a placeholder .env file with proper permissions
130 | RUN touch /app/.env && chmod 644 /app/.env && chown ${USERNAME}:${USERNAME} /app/.env
131 |
132 | # Set executable permissions
133 | RUN chmod +x /app/gomft
134 |
135 | # Set ownership of application files
136 | RUN chown -R ${USERNAME}:${USERNAME} /app
137 |
138 | # Expose the application port
139 | EXPOSE 8080
140 |
141 | # Use our entrypoint script
142 | ENTRYPOINT ["/entrypoint.sh"]
143 |
144 | # Run the application
145 | CMD ["/app/gomft"]
146 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ben Busby
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------
1 | import * as esbuild from 'esbuild';
2 | import path from 'path';
3 | import { fileURLToPath } from 'url';
4 | import { execSync } from 'child_process';
5 | import fs from 'fs';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | const isWatch = process.argv.includes('--watch');
11 |
12 | // Ensure the dist directory exists
13 | const distDir = path.join(__dirname, 'static', 'dist');
14 | if (!fs.existsSync(distDir)) {
15 | fs.mkdirSync(distDir, { recursive: true });
16 | }
17 |
18 | // Copy Font Awesome files
19 | const fontAwesomeSrcDir = path.join(__dirname, 'node_modules', '@fortawesome', 'fontawesome-free');
20 | const fontAwesomeDestDir = path.join(distDir, 'fontawesome');
21 |
22 | // Copy CSS files
23 | const cssFiles = [
24 | 'css/all.min.css',
25 | 'css/fontawesome.min.css',
26 | 'css/solid.min.css',
27 | 'css/regular.min.css',
28 | 'css/brands.min.css'
29 | ];
30 |
31 | cssFiles.forEach(file => {
32 | const srcFile = path.join(fontAwesomeSrcDir, file);
33 | const destFile = path.join(fontAwesomeDestDir, file);
34 | const destDir = path.dirname(destFile);
35 |
36 | if (!fs.existsSync(destDir)) {
37 | fs.mkdirSync(destDir, { recursive: true });
38 | }
39 |
40 | if (fs.existsSync(srcFile)) {
41 | fs.copyFileSync(srcFile, destFile);
42 | }
43 | });
44 |
45 | // Copy webfonts
46 | const webfontsSrcDir = path.join(fontAwesomeSrcDir, 'webfonts');
47 | const webfontsDestDir = path.join(fontAwesomeDestDir, 'webfonts');
48 |
49 | if (!fs.existsSync(webfontsDestDir)) {
50 | fs.mkdirSync(webfontsDestDir, { recursive: true });
51 | }
52 |
53 | fs.readdirSync(webfontsSrcDir).forEach(file => {
54 | fs.copyFileSync(
55 | path.join(webfontsSrcDir, file),
56 | path.join(webfontsDestDir, file)
57 | );
58 | });
59 |
60 | const commonConfig = {
61 | sourcemap: true,
62 | minify: true,
63 | bundle: true,
64 | platform: 'browser',
65 | target: ['es2020'],
66 | };
67 |
68 | async function buildTailwind() {
69 | console.log('Building Tailwind CSS...');
70 | execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --minify');
71 | }
72 |
73 | async function build() {
74 | try {
75 | // Build vendor JavaScript bundle (CDN dependencies)
76 | await esbuild.build({
77 | ...commonConfig,
78 | entryPoints: ['static/js/vendor.js'],
79 | outfile: 'static/dist/vendor.js',
80 | format: 'iife',
81 | });
82 |
83 | // Build application JavaScript
84 | await esbuild.build({
85 | ...commonConfig,
86 | entryPoints: ['static/js/app.js'],
87 | outfile: 'static/dist/app.js',
88 | format: 'iife',
89 | });
90 |
91 | // Build initialization JavaScript
92 | await esbuild.build({
93 | ...commonConfig,
94 | entryPoints: ['static/js/init.js'],
95 | outfile: 'static/dist/init.js',
96 | format: 'iife',
97 | });
98 |
99 | // Build CSS with Tailwind
100 | await buildTailwind();
101 |
102 | console.log('Build completed successfully!');
103 | } catch (error) {
104 | console.error('Build failed:', error);
105 | process.exit(1);
106 | }
107 | }
108 |
109 | if (isWatch) {
110 | // Watch mode
111 | console.log('Starting watch mode...');
112 | const ctx = await esbuild.context(commonConfig);
113 | await ctx.watch();
114 |
115 | // Watch Tailwind CSS
116 | execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --watch');
117 |
118 | console.log('Watching for changes...');
119 | } else {
120 | // Single build
121 | build();
122 | }
--------------------------------------------------------------------------------
/components/auth_providers_buttons.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "fmt"
5 | "github.com/starfleetcptn/gomft/internal/db"
6 | )
7 |
8 | // getProviderIcon returns the appropriate icon for a provider
9 | func getProviderIcon(provider db.AuthProvider) templ.Component {
10 | // If provider has a custom icon URL, use it
11 | if provider.IconURL != "" {
12 | return templ.Raw(fmt.Sprintf(` `, provider.IconURL, provider.Name))
13 | }
14 |
15 | // Otherwise fall back to default icons based on type
16 | switch provider.Type {
17 | case db.ProviderTypeAuthentik:
18 | return templ.Raw(` `)
19 | case db.ProviderTypeOIDC:
20 | return templ.Raw(` `)
21 | case db.ProviderTypeSAML:
22 | return templ.Raw(` `)
23 | case db.ProviderTypeOAuth2:
24 | return templ.Raw(` `)
25 | default:
26 | return templ.Raw(` `)
27 | }
28 | }
29 |
30 | templ AuthProviderButtons(providers []db.AuthProvider) {
31 | if len(providers) == 0 {
32 |
33 | No external authentication providers available
34 |
35 | } else {
36 |
51 | }
52 | }
--------------------------------------------------------------------------------
/components/components.go:
--------------------------------------------------------------------------------
1 | // Package components contains the UI components for the GoMFT application.
2 | // This file serves as a marker for the components package to ensure it's properly recognized by Go.
3 | package components
--------------------------------------------------------------------------------
/components/file_metadata/dialog/file_metadata_dialog.templ:
--------------------------------------------------------------------------------
1 | package dialog
2 |
3 | import (
4 | "fmt"
5 | "github.com/starfleetcptn/gomft/components/file_metadata/utils"
6 | )
7 |
8 | // FileMetadataDialog renders a confirmation dialog for file metadata actions
9 | templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
10 | @utils.FileMetadataJS()
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | if action == "delete" {
19 |
20 | } else {
21 |
22 | }
23 |
{ message }
24 | if section == "list" {
25 |
35 | { confirmText }
36 |
37 | } else {
38 |
47 | { confirmText }
48 |
49 | }
50 |
51 | Cancel
52 |
53 |
54 |
55 |
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/components/file_metadata/search/file_metadata_search_content.templ:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | import (
4 | "context"
5 | "github.com/starfleetcptn/gomft/components/file_metadata"
6 | "github.com/starfleetcptn/gomft/components/file_metadata/list"
7 | )
8 |
9 | // FileMetadataSearchContent renders only the search results table and pagination
10 | templ FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) {
11 |
12 |
13 | if len(data.Files) > 0 {
14 | @list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
15 | Files: data.Files,
16 | Page: data.Page,
17 | Limit: data.Limit,
18 | TotalCount: data.TotalCount,
19 | TotalPages: data.TotalPages,
20 | Filter: data.Filter, // Pass filter data for pagination links
21 | SortBy: data.SortBy,
22 | SortDir: data.SortDir,
23 | }, "/files/search/partial", "#search-results-container") // Pass correct base path and target ID
24 | } else {
25 |
26 |
27 |
28 |
29 |
No files found matching your search criteria.
30 |
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/components/file_metadata/types.go:
--------------------------------------------------------------------------------
1 | package file_metadata
2 |
3 | import "github.com/starfleetcptn/gomft/internal/db"
4 |
5 | // FileMetadataFilter represents filter parameters for file metadata queries
6 | type FileMetadataFilter struct {
7 | Status string
8 | JobID string
9 | FileName string
10 | Hash string
11 | StartDate string
12 | EndDate string
13 | }
14 |
15 | // FileMetadataListData contains data for the file metadata list template
16 | type FileMetadataListData struct {
17 | Files []db.FileMetadata
18 | TotalCount int64
19 | Page int
20 | Limit int
21 | TotalPages int
22 | Job *db.Job // Optional: if viewing files for a specific job
23 | Filter FileMetadataFilter
24 | SortBy string // Added for sorting
25 | SortDir string // Added for sorting ("asc" or "desc")
26 | }
27 |
28 | // FileMetadataDetailsData contains data for the file metadata details template
29 | type FileMetadataDetailsData struct {
30 | File db.FileMetadata
31 | }
32 |
33 | // FileMetadataSearchData contains data for the file metadata search template
34 | type FileMetadataSearchData struct {
35 | Files []db.FileMetadata
36 | TotalCount int64
37 | Page int
38 | Limit int
39 | TotalPages int
40 | Filter FileMetadataFilter
41 | SortBy string // Added for sorting
42 | SortDir string // Added for sorting ("asc" or "desc")
43 | }
44 |
--------------------------------------------------------------------------------
/components/file_metadata/utils/file_metadata_utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "fmt"
4 |
5 | // GetStatusBadgeClass returns the appropriate CSS class for a file status badge
6 | func GetStatusBadgeClass(status string) string {
7 | switch status {
8 | case "processed":
9 | return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
10 | case "archived":
11 | return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
12 | case "deleted":
13 | return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
14 | case "archived_and_deleted":
15 | return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300"
16 | case "error":
17 | return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
18 | default:
19 | return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
20 | }
21 | }
22 |
23 | // FormatFileSize formats a file size in bytes to a human-readable string
24 | func FormatFileSize(size int64) string {
25 | if size < 1024 {
26 | return fmt.Sprintf("%d B", size)
27 | } else if size < 1024*1024 {
28 | return fmt.Sprintf("%.2f KB", float64(size)/1024)
29 | } else if size < 1024*1024*1024 {
30 | return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
31 | } else {
32 | return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/notifications/dialog/dialog.templ:
--------------------------------------------------------------------------------
1 | package dialog
2 |
3 | import (
4 | "fmt"
5 | // "strconv" // No longer needed here
6 | )
7 |
8 | // NotificationDialog component for confirmation dialogs using Flowbite modal
9 | templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | if action == "delete" {
18 |
19 | } else {
20 |
21 | }
22 |
{ message }
23 |
32 | { confirmText }
33 |
34 |
35 | Cancel
36 |
37 |
38 |
39 |
40 |
41 | }
42 |
43 | // Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
--------------------------------------------------------------------------------
/components/notifications/dialog/dialog_js.templ:
--------------------------------------------------------------------------------
1 | package dialog
2 |
3 | // DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
4 | templ DialogScripts() {
5 |
20 | }
--------------------------------------------------------------------------------
/components/notifications/form/fields/email.templ:
--------------------------------------------------------------------------------
1 | package fields
2 |
3 | import (
4 | "github.com/starfleetcptn/gomft/components/notifications/types"
5 | // No utils needed for this specific template yet
6 | )
7 |
8 | templ EmailFields(data types.NotificationFormData) {
9 |
10 |
32 | }
--------------------------------------------------------------------------------
/components/notifications/form/form_js.templ:
--------------------------------------------------------------------------------
1 | package form
2 |
3 | // FormScripts contains JavaScript specific to the notification form page.
4 | templ FormScripts() {
5 |
45 | }
--------------------------------------------------------------------------------
/components/notifications/form/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // GetNotificationFormTitle returns the title for the notification form page.
8 | func GetNotificationFormTitle(isNew bool) string {
9 | if isNew {
10 | return "Add Notification Service"
11 | }
12 | return "Edit Notification Service"
13 | }
14 |
15 | // Contains checks if a string slice contains a specific string.
16 | func Contains(slice []string, item string) bool {
17 | for _, a := range slice {
18 | if a == item {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
25 | // BoolToString converts a boolean to its string representation "true" or "false".
26 | // Useful for setting HTML attributes that expect string values.
27 | func BoolToString(b bool) string {
28 | if b {
29 | return "true"
30 | }
31 | return "false"
32 | }
33 |
34 | // IsEventTriggerSelected checks if a specific event trigger should be pre-selected.
35 | // It now accepts the anonymous struct type defined in types.NotificationFormData.
36 | // Defaults to checking 'job_complete' and 'job_error' when creating a new service.
37 | func IsEventTriggerSelected(service *struct {
38 | ID uint
39 | Name string
40 | Description string
41 | Type string
42 | IsEnabled bool
43 | EventTriggers []string
44 | RetryPolicy string
45 | WebhookURL string
46 | Method string
47 | Headers string
48 | PayloadTemplate string
49 | SecretKey string
50 | PushbulletAPIKey string
51 | PushbulletDeviceID string
52 | PushbulletTitleTemplate string
53 | PushbulletBodyTemplate string
54 | NtfyServer string
55 | NtfyTopic string
56 | NtfyPriority string
57 | NtfyUsername string
58 | NtfyPassword string
59 | NtfyTitleTemplate string
60 | NtfyMessageTemplate string
61 | GotifyURL string
62 | GotifyToken string
63 | GotifyPriority string
64 | GotifyTitleTemplate string
65 | GotifyMessageTemplate string
66 | PushoverAPIToken string
67 | PushoverUserKey string
68 | PushoverDevice string
69 | PushoverPriority string
70 | PushoverSound string
71 | PushoverTitleTemplate string
72 | PushoverMessageTemplate string
73 | }, event string, isNew bool) bool {
74 | if !isNew && service != nil {
75 | // Use the Contains helper function
76 | return Contains(service.EventTriggers, event)
77 | }
78 | // Default for new services: check complete and error
79 | return isNew && (event == "job_complete" || event == "job_error")
80 | }
81 |
82 | // FormatEventTriggerName converts event trigger keys to human-readable names.
83 | func FormatEventTriggerName(event string) string {
84 | // Replace underscores with spaces and capitalize words
85 | name := strings.ReplaceAll(event, "_", " ")
86 | name = strings.Title(name) // Use strings.Title for capitalization
87 | return name
88 | }
89 |
--------------------------------------------------------------------------------
/components/notifications/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | // SettingsNotificationsData defines the data needed for the notifications list page
4 | type SettingsNotificationsData struct {
5 | NotificationServices []NotificationServiceData
6 | SuccessMessage string
7 | ErrorMessage string
8 | }
9 |
10 | // NotificationServiceData defines the data for a single service in the list
11 | type NotificationServiceData struct {
12 | ID uint
13 | Name string
14 | Type string
15 | IsEnabled bool
16 | Config map[string]string // Keep for now, might refine later
17 | Description string
18 | EventTriggers []string
19 | PayloadTemplate string
20 | SecretKey string
21 | RetryPolicy string
22 | SuccessCount int
23 | FailureCount int
24 | }
25 |
26 | // NotificationFormData defines the data needed for the notification add/edit form
27 | // TODO: This will be moved from notification_form.templ later
28 | type NotificationFormData struct {
29 | NotificationService *struct {
30 | ID uint
31 | Name string
32 | Description string
33 | Type string
34 | IsEnabled bool
35 | EventTriggers []string
36 | RetryPolicy string
37 | WebhookURL string
38 | Method string
39 | Headers string
40 | PayloadTemplate string
41 | SecretKey string
42 | PushbulletAPIKey string
43 | PushbulletDeviceID string
44 | PushbulletTitleTemplate string
45 | PushbulletBodyTemplate string
46 | NtfyServer string
47 | NtfyTopic string
48 | NtfyPriority string
49 | NtfyUsername string
50 | NtfyPassword string
51 | NtfyTitleTemplate string
52 | NtfyMessageTemplate string
53 | GotifyURL string
54 | GotifyToken string
55 | GotifyPriority string
56 | GotifyTitleTemplate string
57 | GotifyMessageTemplate string
58 | PushoverAPIToken string
59 | PushoverUserKey string
60 | PushoverDevice string
61 | PushoverPriority string
62 | PushoverSound string
63 | PushoverTitleTemplate string
64 | PushoverMessageTemplate string
65 | }
66 | IsNew bool
67 | SuccessMessage string
68 | ErrorMessage string
69 | }
70 |
--------------------------------------------------------------------------------
/components/providers/destination/destination.go:
--------------------------------------------------------------------------------
1 | // Package destination contains the UI destination forms for the GoMFT application.
2 | // This file serves as a marker for the destination package to ensure it's properly recognized by Go.
3 | package destination
4 |
--------------------------------------------------------------------------------
/components/providers/destination/local.templ:
--------------------------------------------------------------------------------
1 | package destination
2 |
3 | templ LocalDestinationForm() {
4 |
5 |
6 |
Local Path
7 |
16 |
Full path to the local directory containing your files
17 |
23 |
29 |
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/components/providers/destination/nextcloud.templ:
--------------------------------------------------------------------------------
1 | package destination
2 |
3 | templ NextCloudDestinationForm() {
4 |
5 |
6 |
7 |
8 | Configure your NextCloud connection details below. You'll need your server URL, username, and password.
9 |
10 |
11 |
12 |
13 |
NextCloud URL
14 |
22 |
23 | Full URL to your NextCloud server including protocol (https://)
24 |
25 |
26 |
27 |
28 |
Username
29 |
37 |
38 | Your NextCloud username
39 |
40 |
41 |
42 |
43 |
Password
44 |
52 |
53 | Your NextCloud account password
54 |
55 |
56 |
57 |
58 |
Remote Path
59 |
67 |
68 | Path to your files in NextCloud
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Connection Tip
77 |
If you have two-factor authentication enabled on your NextCloud account, you'll need to create an app password in your NextCloud security settings.
78 |
79 |
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/components/providers/destination/webdav.templ:
--------------------------------------------------------------------------------
1 | package destination
2 |
3 | templ WebDAVDestinationForm() {
4 |
5 |
6 |
7 |
8 | Configure your WebDAV connection details below. You'll need the server URL, username, and password.
9 |
10 |
11 |
12 |
13 |
WebDAV URL
14 |
22 |
23 | Full URL to your WebDAV server including protocol (https://)
24 |
25 |
26 |
27 |
28 |
Username
29 |
37 |
38 | Your WebDAV username
39 |
40 |
41 |
42 |
43 |
Password
44 |
52 |
53 | Your WebDAV account password
54 |
55 |
56 |
57 |
58 |
Remote Path
59 |
67 |
68 | Path to your files on the WebDAV server
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Integration Note
77 |
WebDAV is supported by many services including ownCloud, Nextcloud, Box, and many others. Make sure your WebDAV URL includes the full path to the WebDAV interface.
78 |
79 |
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/components/providers/providers.go:
--------------------------------------------------------------------------------
1 | // Package providers contains the UI providers for the GoMFT application.
2 | // This file serves as a marker for the providers package to ensure it's properly recognized by Go.
3 | package providers
4 |
--------------------------------------------------------------------------------
/components/providers/source/local.templ:
--------------------------------------------------------------------------------
1 | package source
2 |
3 | templ LocalSourceForm() {
4 |
5 |
6 |
Local Path
7 |
16 |
Full path to the local directory containing your files
17 |
23 |
29 |
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/components/providers/source/nextcloud.templ:
--------------------------------------------------------------------------------
1 | package source
2 |
3 | templ NextCloudSourceForm() {
4 |
5 |
6 |
7 |
8 | Configure your NextCloud connection details below. You'll need your server URL, username, and password.
9 |
10 |
11 |
12 |
13 |
NextCloud URL
14 |
22 |
23 | Full URL to your NextCloud server including protocol (https://)
24 |
25 |
26 |
27 |
28 |
Username
29 |
37 |
38 | Your NextCloud username
39 |
40 |
41 |
42 |
43 |
Password
44 |
52 |
53 | Your NextCloud account password
54 |
55 |
56 |
57 |
58 |
Remote Path
59 |
67 |
68 | Path to your files in NextCloud
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Connection Tip
77 |
If you have two-factor authentication enabled on your NextCloud account, you'll need to create an app password in your NextCloud security settings.
78 |
79 |
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/components/providers/source/source.go:
--------------------------------------------------------------------------------
1 | // Package source contains the UI source forms for the GoMFT application.
2 | // This file serves as a marker for the source package to ensure it's properly recognized by Go.
3 | package source
4 |
--------------------------------------------------------------------------------
/components/providers/source/webdav.templ:
--------------------------------------------------------------------------------
1 | package source
2 |
3 | templ WebDAVSourceForm() {
4 |
5 |
6 |
7 |
8 | Configure your WebDAV connection details below. You'll need the server URL, username, and password.
9 |
10 |
11 |
12 |
13 |
WebDAV URL
14 |
22 |
23 | Full URL to your WebDAV server including protocol (https://)
24 |
25 |
26 |
27 |
28 |
Username
29 |
37 |
38 | Your WebDAV username
39 |
40 |
41 |
42 |
43 |
Password
44 |
52 |
53 | Your WebDAV account password
54 |
55 |
56 |
57 |
58 |
Remote Path
59 |
67 |
68 | Path to your files on the WebDAV server
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Integration Note
77 |
WebDAV is supported by many services including ownCloud, Nextcloud, Box, and many others. Make sure your WebDAV URL includes the full path to the WebDAV interface.
78 |
79 |
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/components/shared/toast/toast.templ:
--------------------------------------------------------------------------------
1 | package toast
2 |
3 | templ Container() {
4 |
5 | }
--------------------------------------------------------------------------------
/components/shared/toast/toast_js.templ:
--------------------------------------------------------------------------------
1 | package toast
2 |
3 | templ ShowToastJS() {
4 |
93 | }
--------------------------------------------------------------------------------
/components/test_result.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ TestResult(success bool, message string) {
4 | if success {
5 |
6 |
7 | { message }
8 |
9 | } else {
10 |
11 |
12 | { message }
13 |
14 | }
15 | }
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | gomft:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | args:
7 | # Set the UID/GID to match your host user for better file permissions
8 | # Default is 1000:1000 if not specified
9 | UID: ${UID:-1000}
10 | GID: ${GID:-1000}
11 | # Version information
12 | VERSION: ${VERSION:-dev}
13 | BUILD_TIME: ${BUILD_TIME:-unknown}
14 | COMMIT: ${COMMIT:-unknown}
15 | container_name: gomft
16 | restart: unless-stopped
17 | ports:
18 | - "8080:8080"
19 | volumes:
20 | # Main data directory - contains DB and configs
21 | - gomft-data:/app/data
22 | # Separate backups directory
23 | - gomft-backups:/app/backups
24 | # For development, you can mount the source code
25 | # - .:/app
26 | environment:
27 | - TZ=UTC
28 | - DATA_DIR=/app/data
29 | - BACKUP_DIR=/app/backups
30 | - LOGS_DIR=/app/data/logs
31 | # - LOG_LEVEL=info
32 | networks:
33 | - gomft-network
34 | # For non-root installs, the container needs to run with the same UID
35 | # as the host user to access mounted volumes properly
36 | user: ${UID:-1000}:${GID:-1000}
37 |
38 | networks:
39 | gomft-network:
40 | driver: bridge
41 |
42 | volumes:
43 | gomft-data:
44 | driver: local
45 | gomft-backups:
46 | driver: local
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # Keep the screenshots directory README
23 | !/static/screenshots/README.md
24 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # GoMFT Documentation
2 |
3 | This repository contains the documentation for [GoMFT](https://github.com/StarFleetCPTN/GoMFT), a modern, web-based managed file transfer solution written in Go.
4 |
5 | [](https://discord.gg/f9dwtM3j)
6 |
7 | ## Getting Started
8 |
9 | ### Installation
10 |
11 | ```bash
12 | # Install dependencies
13 | npm install
14 |
15 | # Start the development server
16 | npm run start
17 | ```
18 |
19 | ### Build
20 |
21 | ```bash
22 | # Build the static site
23 | npm run build
24 | ```
25 |
26 | The built files will be in the `build` directory.
27 |
28 | ## Documentation Structure
29 |
30 | - **Introduction**: Overview and features of GoMFT
31 | - **Getting Started**: Installation guides and quick start
32 | - **Core Concepts**: Detailed information about transfers, connections, and schedules
33 | - **Advanced Features**: Webhooks, email notifications, and admin tools
34 | - **Security**: Security best practices and configuration
35 | - **Development**: Project structure and contributing guidelines
36 |
37 | ## Contributing
38 |
39 | Contributions to the documentation are welcome! Please submit a PR with your changes.
40 |
41 | ## Community
42 |
43 | Join our [Discord community](https://discord.gg/f9dwtM3j) for support, discussions, and updates about GoMFT.
44 |
45 | ## License
46 |
47 | This documentation is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
48 |
--------------------------------------------------------------------------------
/docs/docs/advanced/gotify-notifications.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | title: Gotify Notifications
4 | ---
5 |
6 | # Gotify Notifications
7 |
8 | 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.
9 |
10 | ## Overview
11 |
12 | Gotify integration allows GoMFT to:
13 |
14 | - Send push notifications to your self-hosted Gotify server
15 | - Customize notification priority based on event importance
16 | - Include detailed transfer information in notifications
17 | - Support private and secure notification delivery
18 |
19 | ## Prerequisites
20 |
21 | Before setting up Gotify notifications in GoMFT, you need:
22 |
23 | 1. A running Gotify server (self-hosted)
24 | 2. An application token from your Gotify server
25 | 3. Network connectivity between GoMFT and the Gotify server
26 |
27 | ## Configuration
28 |
29 | ### Global Gotify Settings
30 |
31 | To configure Gotify notifications:
32 |
33 | 1. Navigate to **Settings** > **Notification Services** > **Add New** > **Gotify**
34 | 2. Configure the following settings:
35 | - **Gotify Server URL**: The URL of your Gotify server (e.g., `https://gotify.example.com`)
36 | - **Application Token**: The token for your GoMFT application in Gotify
37 | - **Default Priority**: The default priority level for notifications (1-10)
38 | - **Verify SSL**: Whether to verify SSL certificates (recommended for production)
39 |
40 | ### Testing Gotify Connection
41 |
42 | After configuring your Gotify settings:
43 |
44 | 1. Click **Test Connection** to verify connectivity with your Gotify server
45 | 2. Click **Send Test Notification** to send a test message
46 |
47 | ## Notification Content
48 |
49 | ### Priority Levels
50 |
51 | Gotify uses numeric priority levels that GoMFT leverages for different event types:
52 |
53 | | Priority | Usage in GoMFT |
54 | |----------|----------------|
55 | | 1-3 | Low priority: successful transfers, routine events |
56 | | 4-7 | Medium priority: warnings, transfers with issues |
57 | | 8-10 | High priority: failed transfers, critical system issues |
58 |
59 | ### Example Notifications
60 |
61 | GoMFT sends structured notifications with helpful information:
62 |
63 | #### Successful Transfer
64 |
65 | ```
66 | Title: Transfer Completed: Daily Backup
67 | Message: Successfully transferred 123 files (1.45 GB) in 2:15
68 | Priority: 3
69 | ```
70 |
71 | #### Failed Transfer
72 |
73 | ```
74 | Title: Transfer Failed: Daily Backup
75 | Message: Error: Connection refused to destination server
76 | Files processed: 45/123
77 | Size transferred: 0.5/1.45 GB
78 | Priority: 8
79 | ```
80 |
81 | ## Troubleshooting
82 |
83 | ### Common Issues
84 |
85 | - **Connection Refused**: Ensure the Gotify server URL is correct and accessible
86 | - **Authentication Failed**: Verify the Application Token is correct
87 | - **SSL Certificate Errors**: Check the Verify SSL setting and certificate validity
88 |
89 | ### Gotify Logs
90 |
91 | To troubleshoot notification issues:
92 |
93 | 1. Check the GoMFT logs: **Admin Tools** > **Logs** > filter for "gotify"
94 | 2. Review the Gotify server logs for any errors
95 | 3. Verify network connectivity between GoMFT and the Gotify server
96 |
97 | ## Best Practices
98 |
99 | - **Use HTTPS** for your Gotify server to ensure secure communication
100 | - **Set Appropriate Priorities** to differentiate between routine and critical notifications
101 | - **Use Client Applications** on your devices to receive Gotify notifications
102 | - **Set Up Multiple Notification Methods** for critical events
--------------------------------------------------------------------------------
/docs/docs/advanced/pushbullet-notifications.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | title: Pushbullet Notifications
4 | ---
5 |
6 | # Pushbullet Notifications
7 |
8 | 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.
9 |
10 | ## Overview
11 |
12 | Pushbullet integration in GoMFT offers:
13 |
14 | - Cross-platform notifications across your devices (Android, iOS, Chrome, Firefox, etc.)
15 | - Option to send to all devices or specific devices
16 | - Rich notification content with transfer details
17 | - Support for notification mirroring between devices
18 |
19 | ## Prerequisites
20 |
21 | Before configuring Pushbullet notifications in GoMFT, you need:
22 |
23 | 1. A Pushbullet account
24 | 2. Pushbullet API access token
25 | 3. Pushbullet app installed on your devices
26 |
27 | ## Configuration
28 |
29 | ### Global Pushbullet Settings
30 |
31 | To configure Pushbullet notifications:
32 |
33 | 1. Navigate to **Settings** > **Notification Services** > **Add New** > **Pushbullet**
34 | 2. Configure the following settings:
35 | - **API Token**: Your Pushbullet access token
36 | - **Default Device**: The device identifier to send notifications to (optional)
37 | - **Default Type**: "Note" (default) or "Link"
38 |
39 | ### Getting Your Pushbullet API Token
40 |
41 | 1. Log in to your Pushbullet account at [pushbullet.com](https://www.pushbullet.com/)
42 | 2. Go to **Settings** > **Account**
43 | 3. In the **Access Tokens** section, click **Create Access Token**
44 | 4. Copy the generated token and paste it into GoMFT
45 |
46 | ### Testing Pushbullet Connection
47 |
48 | After configuring your Pushbullet settings:
49 |
50 | 1. Click **Send Test Notification** to send a test notification to your devices
51 |
52 | ## Notification Content
53 |
54 | ### Notification Types
55 |
56 | GoMFT supports two types of Pushbullet notifications:
57 |
58 | #### Note Type
59 |
60 | Simple notifications with a title and body:
61 |
62 | ```
63 | Title: Transfer Complete: Daily Backup
64 | Body: Successfully transferred 123 files (1.45 GB) in 2:15
65 | ```
66 |
67 | #### Link Type
68 |
69 | Notifications that include a link to the GoMFT interface:
70 |
71 | ```
72 | Title: Transfer Failed: Daily Backup
73 | Body: Error: Connection refused to destination server
74 | URL: https://gomft.example.com/transfers/123
75 | ```
76 |
77 | ### Example Notifications
78 |
79 | #### Successful Transfer
80 |
81 | ```
82 | Title: Transfer Complete: Daily Backup
83 | Body: Transfer completed successfully at 2023-09-15 14:22:33
84 | Files: 123
85 | Size: 1.45 GB
86 | Duration: 00:02:15
87 | ```
88 |
89 | #### Failed Transfer
90 |
91 | ```
92 | Title: Transfer Failed: Daily Backup
93 | Body: Transfer failed with error: Connection refused
94 | Files Processed: 45/123
95 | Size Transferred: 0.5/1.45 GB
96 | Duration: 00:01:05
97 | Error: Failed to connect to destination server
98 | URL: https://gomft.example.com/transfers/123
99 | ```
100 |
101 | ## Troubleshooting
102 |
103 | ### Common Issues
104 |
105 | - **API Token Errors**: Verify your Pushbullet API token is correct
106 | - **No Notifications Arriving**: Check device connectivity and Pushbullet app settings
107 | - **Rate Limiting**: Pushbullet has API rate limits; spread out notification frequency
108 | - **Device Selection Issues**: Verify device identifiers if targeting specific devices
109 |
110 | ### Pushbullet Logs
111 |
112 | To troubleshoot notification issues:
113 |
114 | 1. Check the GoMFT logs: **Administration** > **Log Viewer** > filter for "pushbullet"
115 | 2. Review the Pushbullet account activity in your Pushbullet account
116 |
117 | ## Best Practices
118 |
119 | - **Secure Your API Token**: Treat your Pushbullet API token as sensitive information
120 | - **Group Related Notifications** to avoid notification fatigue
121 | - **Include Action Links** for quick access to relevant GoMFT pages
122 | - **Set Up Multiple Notification Methods** for critical systems
123 |
124 | ## Pushbullet Alternatives
125 |
126 | If you encounter limitations with Pushbullet, GoMFT also supports:
127 |
128 | - [Email Notifications](./email-notifications)
129 | - [Gotify](./gotify-notifications)
130 | - [Ntfy](./ntfy-notifications)
131 | - [Pushover](./pushover-notifications)
--------------------------------------------------------------------------------
/docs/docs/core-concepts/monitoring.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | title: Monitoring
4 | ---
5 |
6 | # Monitoring and Reporting
7 |
8 | 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.
9 |
10 | ## Dashboard
11 |
12 | The GoMFT dashboard provides a real-time overview of your file transfer system:
13 |
14 | ### Dashboard Components
15 |
16 | - **Transfer Status**: Overview of currently running, completed, and failed transfers
17 | - **Recent Transfers**: List of the most recent transfer executions
18 | - **System Health**: Indicators for system health and resource usage
19 | - **Quick Actions**: Buttons for common tasks like creating transfers or checking logs
20 |
21 | To access the dashboard:
22 | 1. Log in to GoMFT
23 | 2. The dashboard is the default landing page
24 | 3. You can return to it anytime by clicking **Dashboard** in the sidebar
25 |
26 | ## Transfer History
27 |
28 | The transfer history section provides detailed information about all transfer executions:
29 |
30 | ### Transfer History Features
31 |
32 | - **Comprehensive Logs**: Complete transfer history with filtering options
33 | - **Status Tracking**: Visual indicators for transfer status (successful, failed, in progress)
34 | - **Performance Metrics**: Data on transfer speed, file counts, and total bytes
35 | - **Time Tracking**: Start time, end time, and duration for all transfers
36 | - **Filter and Search**: Find specific transfers by name, status, date, or other criteria
37 |
38 | To access transfer history:
39 | 1. Navigate to **Transfer History** in the sidebar
40 | 2. Use filters to narrow down the list of transfers
41 | 3. Click on any transfer to see detailed information
42 |
43 | ## Real-Time Monitoring
44 |
45 | GoMFT provides real-time monitoring of active transfers:
46 |
47 | ### Active Transfers
48 |
49 | - **Live Progress**: See transfer progress as it happens
50 | - **File Counters**: Track files transferred, remaining, and skipped
51 | - **Bandwidth Usage**: Monitor current transfer speeds
52 | - **Cancel Option**: Ability to cancel running transfers
53 | - **Log Streaming**: View logs as they're generated
54 |
55 | To monitor active transfers:
56 | 1. Navigate to **Transfer History** in the sidebar
57 | 2. View all currently running transfers
58 | 3. Click on any transfer to see detailed progress
59 |
60 | ## Detailed Transfer Logs
61 |
62 | For each transfer execution, GoMFT maintains detailed logs:
63 |
64 | ### Log Information
65 |
66 | - **File Details**: Information about each transferred file
67 | - **Error Messages**: Detailed error information for failed transfers
68 | - **Warning Messages**: Warnings that occurred during transfer
69 | - **Transfer Summary**: Overall summary of the transfer operation
70 | - **Performance Data**: Transfer rates and timing information
71 |
72 | To access detailed logs:
73 | 1. Navigate to **Transfer History** in the sidebar
74 | 2. Find the transfer of interest
75 | 3. Click on **View Details** to open details
76 |
77 | ## System Monitoring
78 |
79 | GoMFT monitors the health and performance of the system itself:
80 |
81 | ## Alerts and Notifications
82 |
83 | GoMFT can alert you to important events:
84 |
85 | ### Alert Types
86 |
87 | - **Transfer Failures**: Notifications when transfers fail
88 | - **Transfer Completion**: Alerts when transfers complete
89 |
90 | To configure alerts:
91 | 1. Navigate to **Notification Providers**
92 | 2. Set up notification methods (email, webhook, gotify, ntfy, pushover, pushbullet)
93 | 4. Configure alert severity levels
94 |
95 | ## Export and API Access
96 |
97 | GoMFT allows you to export monitoring data:
98 |
99 | ### Export Options
100 |
101 | - **CSV Export**: Download transfer history in CSV format
102 | - **JSON Export**: Export data in JSON format for further processing
103 |
104 | ## Best Practices
105 |
106 | - **Review the dashboard daily** to stay informed of transfer status
107 | - **Set up alerts** for critical transfers to be notified of failures
108 | - **Generate regular reports** for compliance and performance tracking
109 | - **Monitor system health** to prevent resource issues
110 | - **Archive logs** for long-term storage and compliance
111 | - **Use filters** to focus on the most important information
112 | - **Export data** for backup and external analysis
--------------------------------------------------------------------------------
/docs/docs/getting-started/quick-start.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: Quick Start
4 | ---
5 |
6 | # GoMFT Quick Start Guide
7 |
8 | 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.
9 |
10 | ## Accessing the Web Interface
11 |
12 | After installation, access the GoMFT web interface at `http://your-server:8080` (or the appropriate port if you've modified it).
13 |
14 | 1. Log in with the default credentials:
15 | - **Username**: admin@example.com
16 | - **Password**: admin
17 |
18 | ## Initial Dashboard
19 |
20 | The dashboard provides an overview of:
21 | - Recent transfer jobs
22 | - Upcoming scheduled transfers
23 | - System status
24 | - Quick action buttons
25 |
26 | 
27 |
28 | ## Creating Your First Transfer Configuration
29 |
30 | 1. Navigate to **Transfer Configurations** in the sidebar menu
31 | 2. Click **+ New Configuration**
32 | 3. Configure the transfer:
33 | - Select source and destination configurations
34 | - Specify source and destination paths
35 | - Choose the transfer type (Copy, Sync, Move, etc.)
36 | - Configure transfer options (file filtering, bandwidth limits, etc.)
37 | 4. Click **Save Transfer**
38 |
39 | 
40 |
41 | ## Create Your First Scheduled Job
42 | 1. Naviagate to **Scheduled Jobs** in the sidebar menu
43 | 2. Click **+ New Job**
44 | 3. Configure the job:
45 | - Specifiy schedule
46 | - Select Job(s) to run this can be 1 or more
47 | - Change job run order if needed
48 |
49 | ## Running a Transfer
50 |
51 | Once you've created a transfer configuration, you can:
52 |
53 | ### Run On-Demand
54 |
55 | 1. Navigate to **Secheduled Jobs**
56 | 2. Find your transfer in the list
57 | 3. Click the **Run Now** button
58 | 4. The transfer will execute immediately
59 |
60 | ### Schedule a Transfer
61 |
62 | 1. Navigate to **Schedules**
63 | 2. Click **Create New Schedule**
64 | 3. Select your transfer configuration
65 | 4. Set the schedule using cron syntax or the schedule builder
66 | 5. Set additional options (timeout, max retries, etc.)
67 | 6. Click **Save Schedule**
68 |
69 | ## Monitoring Transfers
70 |
71 | 1. Navigate to **Transfer History** to view all past and ongoing transfers
72 | 2. Click on a specific transfer to view detailed information:
73 | - Transfer status
74 | - Start and end times
75 | - Files transferred
76 | - Bytes transferred
77 | - Errors (if any)
78 | - Transfer log
79 |
80 | ## Next Steps
81 |
82 | Now that you've set up your first transfer, explore these additional features:
83 |
84 | - [Docker Deployment](/docs/getting-started/docker) - For containerized deployment
85 | - [Traditional Installation](/docs/getting-started/traditional) - For non-Docker environments
86 | - [Transfer Concepts](/docs/core-concepts/transfers) - Learn more about transfer operations
87 | - [Scheduling](/docs/core-concepts/schedules) - Advanced scheduling options
88 | - [Monitoring](/docs/core-concepts/monitoring) - Advanced monitoring capabilities
--------------------------------------------------------------------------------
/docs/docs/introduction/features.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: Features
4 | ---
5 |
6 | # GoMFT Features
7 |
8 | 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:
9 |
10 | ## Core Features
11 |
12 | ### Multi-Protocol Support
13 |
14 | GoMFT leverages rclone to support a wide range of storage providers and protocols:
15 |
16 | - **Cloud Storage**: Amazon S3, Google Cloud Storage
17 | - **Object Storage**: MinIO, Backblaze B2, Wasabi
18 | - **FTP/SFTP**: FTP, FTPS, SFTP servers
19 | - **WebDAV**: WebDAV servers and services
20 | - **Local Storage**: Local disk, SMB/CIFS shares
21 | - **And many more**: Over 40 storage systems supported
22 |
23 | ### Intuitive Web Interface
24 |
25 | - **Clean, Modern UI**: Easy-to-use web interface built with Tailwind CSS and HTMX
26 | - **Dashboard**: Overview of recent transfers, scheduled jobs, and system status
27 | - **Configuration Manager**: Visual interface for creating and editing transfer configurations
28 | - **Job Scheduler**: Interface for creating and managing scheduled jobs
29 | - **Transfer Logs**: Detailed logs of all transfer operations
30 | - **Dark Mode**: Support for light and dark themes
31 |
32 | ### Powerful Scheduling
33 |
34 | - **Cron-style Scheduling**: Set up transfers using familiar cron syntax
35 | - **Recurring Transfers**: Schedule transfers to run on a regular basis
36 | - **One-time Transfers**: Run transfers immediately or at a specific time
37 | - **Schedule Grouping**: Organize schedules into logical groups
38 | - **Priority Control**: Set priority levels for scheduled tasks
39 |
40 | ## Advanced Features
41 |
42 | ### Transfer Options
43 |
44 | - **Bidirectional Sync**: Synchronize files in both directions
45 | - **File Filtering**: Include or exclude files based on patterns
46 | - **Bandwidth Limiting**: Restrict bandwidth usage for transfers
47 | - **Parallel Transfers**: Configure the number of simultaneous transfers
48 | - **Delta Transfers**: Transfer only changed parts of files
49 | - **Checksumming**: Verify file integrity during transfers
50 |
51 | ### Notification System
52 |
53 | - **Notifications**: Receive alerts when transfers complete or fail
54 | - **Custom Templates**: Customize notification content and format
55 | - **Notification Rules**: Configure which events trigger notifications
56 | - **Notification Providers**: Webhooks, Ntfy, Gotify, Pushover, Pushbullet
57 |
58 | ### Admin Tools
59 |
60 | - **User Management**: Create and manage users with different roles
61 | - **Role-Based Access Control**: Control access to different parts of the application
62 | - **Audit Logging**: Track user actions for security and compliance
63 | - **System Monitoring**: Monitor system performance and resource usage
64 | - **Database Backup/Restore**: Back up and restore the application database
65 | - **Log Viewer**: Browse and search through application logs
66 |
67 | ### Security Features
68 |
69 | - **Authentication**: Secure login with optional MFA support
70 | - **Encryption**: Encrypt data in transit and at rest
71 | - **Secure Credential Storage**: Safely store connection credentials
72 | - **Non-Root Container Support**: Run containers as non-root users for enhanced security
73 |
74 | ## Integration Capabilities
75 |
76 | - **Docker Support**: Easy deployment with Docker containers
77 | - **Docker Compose**: Multi-container deployment using Docker Compose
78 | - **Reverse Proxy Compatible**: Works behind reverse proxies like Nginx or Traefik
--------------------------------------------------------------------------------
/docs/docs/introduction/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: Overview
4 | ---
5 |
6 | # GoMFT Overview
7 |
8 | 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.
9 |
10 | ## What is GoMFT?
11 |
12 | 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.
13 |
14 | ## Key Benefits
15 |
16 | - **User-Friendly Interface**: Intuitive web UI for configuring and monitoring file transfers
17 | - **Multi-Protocol Support**: Transfer files using SFTP, S3, Google Drive, and many more protocols
18 | - **Automated Scheduling**: Set up recurring transfers with flexible scheduling options
19 | - **Comprehensive Logging**: Detailed logs for troubleshooting and audit purposes
20 | - **Notifications**: Get alerts when transfers succeed or fail
21 | - **Docker Support**: Easy deployment with Docker containers
22 | - **Security**: Role-based access control and secure credential management
23 |
24 | ## Use Cases
25 |
26 | - **Data Synchronization**: Keep files in sync across different storage systems
27 | - **Backup and Archiving**: Automate backup processes to cloud or local storage
28 | - **Secure File Exchange**: Transfer files securely between organizations
29 | - **Cloud Migration**: Move data between different cloud providers
30 | - **Workflow Automation**: Trigger file transfers as part of larger workflows
31 | - **Compliance**: Maintain audit logs for regulatory compliance
32 |
33 | GoMFT is designed to be simple to deploy and use, while providing the reliability and features needed for enterprise file transfer needs.
34 |
35 | ## Community Support
36 |
37 | Join our Discord community for support, discussions, and updates about GoMFT:
38 |
39 | Join Discord Community
--------------------------------------------------------------------------------
/docs/fix-image-paths.js:
--------------------------------------------------------------------------------
1 | // Fix image paths in Markdown files
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | // Function to recursively find all Markdown files
6 | function findMarkdownFiles(directory) {
7 | const files = [];
8 |
9 | function traverse(dir) {
10 | const entries = fs.readdirSync(dir, { withFileTypes: true });
11 |
12 | for (const entry of entries) {
13 | const fullPath = path.join(dir, entry.name);
14 |
15 | if (entry.isDirectory()) {
16 | traverse(fullPath);
17 | } else if (entry.isFile() && entry.name.endsWith('.md')) {
18 | files.push(fullPath);
19 | }
20 | }
21 | }
22 |
23 | traverse(directory);
24 | return files;
25 | }
26 |
27 | // Function to fix image paths in a file
28 | function fixImagePaths(filePath) {
29 | let content = fs.readFileSync(filePath, 'utf8');
30 | let originalContent = content;
31 |
32 | // Replace absolute paths with relative ones based on file location
33 | const relativeToRoot = path.relative(path.dirname(filePath), path.resolve('static'));
34 | const relativePath = relativeToRoot.replace(/\\/g, '/');
35 |
36 | // Replace any occurrence of  with 
37 | content = content.replace(/!\[(.*?)\]\(\/img\/(.*?)\)/g,
38 | (match, alt, imgPath) => ``);
39 |
40 | // If content changed, write back to file
41 | if (content !== originalContent) {
42 | console.log(`Fixed image paths in ${filePath}`);
43 | fs.writeFileSync(filePath, content, 'utf8');
44 | return true;
45 | }
46 |
47 | return false;
48 | }
49 |
50 | // Main function
51 | function main() {
52 | console.log('Finding Markdown files...');
53 | const docsDir = path.resolve('docs');
54 | const mdFiles = findMarkdownFiles(docsDir);
55 |
56 | console.log(`Found ${mdFiles.length} Markdown files`);
57 |
58 | let fixedCount = 0;
59 | for (const file of mdFiles) {
60 | if (fixImagePaths(file)) {
61 | fixedCount++;
62 | }
63 | }
64 |
65 | console.log(`Fixed image paths in ${fixedCount} files`);
66 | }
67 |
68 | main();
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "go-mft-docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc",
16 | "deploy-gh-pages": "GIT_USER=StarFleetCPTN USE_SSH=true yarn deploy",
17 | "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); } }); });\"",
18 | "fix-image-paths": "node fix-image-paths.js"
19 | },
20 | "dependencies": {
21 | "@docusaurus/core": "3.7.0",
22 | "@docusaurus/preset-classic": "3.7.0",
23 | "@easyops-cn/docusaurus-search-local": "^0.49.2",
24 | "@mdx-js/react": "^3.0.0",
25 | "clsx": "^2.0.0",
26 | "prism-react-renderer": "^2.3.0",
27 | "react": "^19.0.0",
28 | "react-dom": "^19.0.0"
29 | },
30 | "devDependencies": {
31 | "@docusaurus/module-type-aliases": "3.7.0",
32 | "@docusaurus/tsconfig": "3.7.0",
33 | "@docusaurus/types": "3.7.0",
34 | "typescript": "~5.6.2"
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.5%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 3 chrome version",
44 | "last 3 firefox version",
45 | "last 5 safari version"
46 | ]
47 | },
48 | "engines": {
49 | "node": ">=18.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/docs/sidebars.ts:
--------------------------------------------------------------------------------
1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
2 |
3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
4 |
5 | /**
6 | * Creating a sidebar enables you to:
7 | - create an ordered group of docs
8 | - render a sidebar for each doc of that group
9 | - provide next/previous navigation
10 |
11 | The sidebars can be generated from the filesystem, or explicitly defined here.
12 |
13 | Create as many sidebars as you want.
14 | */
15 | const sidebars: SidebarsConfig = {
16 | docsSidebar: [
17 | {
18 | type: 'category',
19 | label: 'Introduction',
20 | items: ['introduction/overview', 'introduction/features'],
21 | },
22 | {
23 | type: 'category',
24 | label: 'Getting Started',
25 | items: ['getting-started/installation', 'getting-started/configuration', 'getting-started/quick-start', 'getting-started/docker', 'getting-started/traditional'],
26 | },
27 | {
28 | type: 'category',
29 | label: 'Core Concepts',
30 | items: ['core-concepts/transfers', 'core-concepts/connections', 'core-concepts/schedules', 'core-concepts/monitoring'],
31 | },
32 | {
33 | type: 'category',
34 | label: 'Advanced Features',
35 | items: [
36 | 'advanced/notifications-overview',
37 | 'advanced/gotify-notifications',
38 | 'advanced/ntfy-notifications',
39 | 'advanced/pushbullet-notifications',
40 | 'advanced/pushover-notifications',
41 | 'advanced/webhook-notifications',
42 | 'advanced/admin-tools'
43 | ],
44 | },
45 | {
46 | type: 'category',
47 | label: 'Security',
48 | items: ['security/best-practices', 'security/authentication', 'security/non-root'],
49 | },
50 | {
51 | type: 'category',
52 | label: 'Development',
53 | items: ['development/project-structure', 'development/contributing'],
54 | },
55 | ],
56 | };
57 |
58 | export default sidebars;
59 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.tsx:
--------------------------------------------------------------------------------
1 | import type {ReactNode} from 'react';
2 | import clsx from 'clsx';
3 | import Heading from '@theme/Heading';
4 | import styles from './styles.module.css';
5 |
6 | type FeatureItem = {
7 | title: string;
8 | Svg: React.ComponentType>;
9 | description: ReactNode;
10 | };
11 |
12 | const FeatureList: FeatureItem[] = [
13 | {
14 | title: 'Easy to Use',
15 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
16 | description: (
17 | <>
18 | Docusaurus was designed from the ground up to be easily installed and
19 | used to get your website up and running quickly.
20 | >
21 | ),
22 | },
23 | {
24 | title: 'Focus on What Matters',
25 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
26 | description: (
27 | <>
28 | Docusaurus lets you focus on your docs, and we'll do the chores. Go
29 | ahead and move your docs into the docs
directory.
30 | >
31 | ),
32 | },
33 | {
34 | title: 'Powered by React',
35 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
36 | description: (
37 | <>
38 | Extend or customize your website layout by reusing React. Docusaurus can
39 | be extended while reusing the same header and footer.
40 | >
41 | ),
42 | },
43 | ];
44 |
45 | function Feature({title, Svg, description}: FeatureItem) {
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
{title}
53 |
{description}
54 |
55 |
56 | );
57 | }
58 |
59 | export default function HomepageFeatures(): ReactNode {
60 | return (
61 |
62 |
63 |
64 | {FeatureList.map((props, idx) => (
65 |
66 | ))}
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | width: 100%;
6 | }
7 |
8 | .featureSvg {
9 | height: 200px;
10 | width: 200px;
11 | }
12 |
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 996px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | gap: 1rem;
24 | }
25 |
26 | .features {
27 | display: flex;
28 | align-items: center;
29 | padding: 2rem 0;
30 | width: 100%;
31 | }
32 |
33 | .section {
34 | padding: 4rem 0;
35 | }
36 |
37 | .sectionAlt {
38 | background-color: var(--ifm-color-emphasis-100);
39 | }
40 |
--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Markdown page example
3 | ---
4 |
5 | # Markdown page example
6 |
7 | You don't need React to write simple standalone pages.
8 |
--------------------------------------------------------------------------------
/docs/src/theme/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | import React, {type ReactNode} from 'react';
2 | import SearchBar from '@theme-original/SearchBar';
3 | import type SearchBarType from '@theme/SearchBar';
4 | import type {WrapperProps} from '@docusaurus/types';
5 |
6 | type Props = WrapperProps;
7 |
8 | export default function SearchBarWrapper(props: Props): ReactNode {
9 | return (
10 | <>
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/favicon.ico
--------------------------------------------------------------------------------
/docs/static/images/dashboard.gomft-038de3445e0d9228c5621ea0b5577c42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/images/dashboard.gomft-038de3445e0d9228c5621ea0b5577c42.png
--------------------------------------------------------------------------------
/docs/static/images/transfer.config.gomft-7583731408b086ca9c7bf3127aca9952.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/images/transfer.config.gomft-7583731408b086ca9c7bf3127aca9952.png
--------------------------------------------------------------------------------
/docs/static/img/README.md:
--------------------------------------------------------------------------------
1 | # Images directory
2 |
3 | This directory contains various images used throughout the documentation, including:
4 |
5 | 1. Logo files
6 | 2. Screenshots
7 | 3. Icons and other assets
8 |
9 | Some screenshots are automatically copied from the main repository's `/screenshots` directory during the build process.
10 |
11 | The automated copy is handled by the `prepare-screenshots` script in package.json.
12 |
13 | ## Required Images
14 |
15 | The following images are used in the documentation:
16 |
17 | - `logo.svg` - The GoMFT logo for the header
18 | - `favicon.ico` - The website favicon
19 | - `docusaurus-social-card.jpg` - Social media preview image
20 | - `dashboard.gomft.png` - Screenshot of the GoMFT dashboard
21 | - `dashboard.dark.gomft.png` - Screenshot of the GoMFT dashboard in dark mode
22 | - `transfer.config.gomft.png` - Screenshot of the transfer configuration
23 | - `scheduled.jobs.gomft.png` - Screenshot of the scheduled jobs
24 | - `transfer.history.gomft.png` - Screenshot of the transfer history
25 | - `user.management.gomft.png` - Screenshot of the user management
26 | - `role.management.gomft.png` - Screenshot of the role management
27 | - `authentication.providers.gomft.png` - Screenshot of the authentication providers
28 | - `notifications.gomft.png` - Screenshot of the notifications
29 | - `notification.service.gomft.png` - Screenshot of the notification service
30 | - `audit.logs.gomft.png` - Screenshot of the audit logs
31 | - `file.details.gomft.png` - Screenshot of the file details
32 | - `file.metadata.gomft.png` - Screenshot of the file metadata
33 | - `database.tools.gomft.png` - Screenshot of the database tools
34 | - `job.run.details.gomft.png` - Screenshot of the job run details
35 |
36 | ## Symlinks for Backward Compatibility
37 |
38 | - `dashboard.png` → `dashboard.gomft.png`
39 | - `create-transfer.png` → `transfer.config.gomft.png`
40 | - `docker.png` - Placeholder for Docker-related screenshots
41 |
42 | ## Adding New Images
43 |
44 | When adding images to this directory:
45 |
46 | 1. Use descriptive filenames
47 | 2. Optimize image size when possible
48 | 3. Use PNG for screenshots and SVG for vector graphics
49 | 4. Include alt text when referencing images in documentation
--------------------------------------------------------------------------------
/docs/static/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/static/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/static/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/static/img/audit.logs.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/audit.logs.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/authentication.providers.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/authentication.providers.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/create-transfer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/create-transfer.png
--------------------------------------------------------------------------------
/docs/static/img/dashboard.dark.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/dashboard.dark.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/dashboard.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/dashboard.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/dashboard.png
--------------------------------------------------------------------------------
/docs/static/img/database.tools.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/database.tools.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/static/img/docker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/docker.png
--------------------------------------------------------------------------------
/docs/static/img/docusaurus-social-card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/docusaurus-social-card.jpg
--------------------------------------------------------------------------------
/docs/static/img/docusaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/docusaurus.png
--------------------------------------------------------------------------------
/docs/static/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/static/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/file.details.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/file.details.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/file.metadata.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/file.metadata.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/gomft-social-card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/gomft-social-card.jpg
--------------------------------------------------------------------------------
/docs/static/img/job.run.details.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/job.run.details.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/logo.png
--------------------------------------------------------------------------------
/docs/static/img/notification.service.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/notification.service.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/notifications.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/notifications.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/role.management.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/role.management.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/scheduled.jobs.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/scheduled.jobs.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/docs/static/img/transfer.config.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/transfer.config.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/transfer.history.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/transfer.history.gomft.png
--------------------------------------------------------------------------------
/docs/static/img/user.management.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/docs/static/img/user.management.gomft.png
--------------------------------------------------------------------------------
/docs/static/screenshots/README.md:
--------------------------------------------------------------------------------
1 | # Screenshots directory
2 |
3 | This directory contains copies of the screenshots from the main repository root. These are used in the documentation site.
4 |
5 | The contents of this directory are automatically updated by the `prepare-screenshots` script in the package.json file.
6 |
7 | Do not manually add files to this directory as they will be overwritten by the scripts.
--------------------------------------------------------------------------------
/docs/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | },
7 | "exclude": [".docusaurus", "build"]
8 | }
9 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/starfleetcptn/gomft
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/a-h/templ v0.3.857
7 | github.com/gin-contrib/sessions v1.0.3
8 | github.com/gin-gonic/gin v1.10.0
9 | github.com/glebarez/sqlite v1.11.0
10 | github.com/go-gormigrate/gormigrate/v2 v2.1.4
11 | github.com/golang-jwt/jwt/v5 v5.2.2
12 | github.com/joho/godotenv v1.5.1
13 | github.com/pquerna/otp v1.4.0
14 | github.com/robfig/cron/v3 v3.0.1
15 | github.com/stretchr/testify v1.10.0
16 | golang.org/x/crypto v0.37.0
17 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
18 | gorm.io/gorm v1.25.12
19 | )
20 |
21 | require (
22 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
23 | github.com/bytedance/sonic v1.13.2 // indirect
24 | github.com/bytedance/sonic/loader v0.2.4 // indirect
25 | github.com/cloudwego/base64x v0.1.5 // indirect
26 | github.com/davecgh/go-spew v1.1.1 // indirect
27 | github.com/dustin/go-humanize v1.0.1 // indirect
28 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect
29 | github.com/gin-contrib/sse v1.0.0 // indirect
30 | github.com/glebarez/go-sqlite v1.21.2 // indirect
31 | github.com/go-playground/locales v0.14.1 // indirect
32 | github.com/go-playground/universal-translator v0.18.1 // indirect
33 | github.com/go-playground/validator/v10 v10.26.0 // indirect
34 | github.com/goccy/go-json v0.10.5 // indirect
35 | github.com/google/uuid v1.3.0 // indirect
36 | github.com/gorilla/context v1.1.2 // indirect
37 | github.com/gorilla/securecookie v1.1.2 // indirect
38 | github.com/gorilla/sessions v1.4.0 // indirect
39 | github.com/gorilla/websocket v1.5.3 // indirect
40 | github.com/jinzhu/inflection v1.0.0 // indirect
41 | github.com/jinzhu/now v1.1.5 // indirect
42 | github.com/json-iterator/go v1.1.12 // indirect
43 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect
44 | github.com/leodido/go-urn v1.4.0 // indirect
45 | github.com/mattn/go-isatty v0.0.20 // indirect
46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
47 | github.com/modern-go/reflect2 v1.0.2 // indirect
48 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
49 | github.com/pmezard/go-difflib v1.0.0 // indirect
50 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
52 | github.com/ugorji/go/codec v1.2.12 // indirect
53 | golang.org/x/arch v0.16.0 // indirect
54 | golang.org/x/net v0.38.0 // indirect
55 | golang.org/x/sys v0.32.0 // indirect
56 | golang.org/x/text v0.24.0 // indirect
57 | google.golang.org/protobuf v1.36.6 // indirect
58 | gopkg.in/yaml.v3 v3.0.1 // indirect
59 | modernc.org/libc v1.22.5 // indirect
60 | modernc.org/mathutil v1.5.0 // indirect
61 | modernc.org/memory v1.5.0 // indirect
62 | modernc.org/sqlite v1.23.1 // indirect
63 | )
64 |
--------------------------------------------------------------------------------
/internal/auth/jwt.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/golang-jwt/jwt/v5"
9 | )
10 |
11 | // Claims represents the JWT claims
12 | type Claims struct {
13 | UserID uint `json:"user_id"`
14 | Email string `json:"email"`
15 | jwt.RegisteredClaims
16 | }
17 |
18 | // GenerateToken creates a new JWT token for a user
19 | func GenerateToken(userID uint, email, secret string, expirationTime time.Duration) (string, error) {
20 | // Create claims with user ID and expiration time
21 | claims := &Claims{
22 | UserID: userID,
23 | Email: email,
24 | RegisteredClaims: jwt.RegisteredClaims{
25 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(expirationTime)),
26 | IssuedAt: jwt.NewNumericDate(time.Now()),
27 | NotBefore: jwt.NewNumericDate(time.Now()),
28 | Issuer: "gomft",
29 | Subject: fmt.Sprintf("%d", userID),
30 | },
31 | }
32 |
33 | // Create token with claims
34 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
35 |
36 | // Sign token with secret
37 | tokenString, err := token.SignedString([]byte(secret))
38 | if err != nil {
39 | return "", err
40 | }
41 |
42 | return tokenString, nil
43 | }
44 |
45 | // ValidateToken validates a JWT token and returns the claims
46 | func ValidateToken(tokenString, secret string) (*Claims, error) {
47 | // Parse token
48 | token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
49 | // Validate signing method
50 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
51 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
52 | }
53 | return []byte(secret), nil
54 | })
55 |
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | // Extract claims
61 | if claims, ok := token.Claims.(*Claims); ok && token.Valid {
62 | return claims, nil
63 | }
64 |
65 | return nil, errors.New("invalid token")
66 | }
67 |
--------------------------------------------------------------------------------
/internal/db/audit_log.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "database/sql/driver"
5 | "encoding/json"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | // AuditLog represents an audit trail entry in the system
12 | type AuditLog struct {
13 | gorm.Model
14 | Action string `gorm:"size:50;not null;index"`
15 | EntityType string `gorm:"size:50;not null;index"`
16 | EntityID uint `gorm:"not null;index"`
17 | UserID uint `gorm:"not null;index"`
18 | Details AuditLogDetails `gorm:"type:json"`
19 | Timestamp time.Time `gorm:"not null;index;default:CURRENT_TIMESTAMP"`
20 | }
21 |
22 | // AuditLogDetails is a custom type for storing audit log details as JSON
23 | type AuditLogDetails map[string]interface{}
24 |
25 | // Scan implements the sql.Scanner interface
26 | func (d *AuditLogDetails) Scan(value interface{}) error {
27 | if value == nil {
28 | *d = make(AuditLogDetails)
29 | return nil
30 | }
31 |
32 | bytes, ok := value.([]byte)
33 | if !ok {
34 | return nil
35 | }
36 |
37 | return json.Unmarshal(bytes, d)
38 | }
39 |
40 | // Value implements the driver.Valuer interface
41 | func (d AuditLogDetails) Value() (driver.Value, error) {
42 | if d == nil {
43 | return json.Marshal(make(map[string]interface{}))
44 | }
45 | return json.Marshal(d)
46 | }
47 |
--------------------------------------------------------------------------------
/internal/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/glebarez/sqlite"
9 | "github.com/starfleetcptn/gomft/internal/db/migrations"
10 | "gorm.io/gorm"
11 | )
12 |
13 | type DB struct {
14 | *gorm.DB
15 | }
16 |
17 | func Initialize(dbPath string) (*DB, error) {
18 | // Create directory if it doesn't exist
19 | dir := filepath.Dir(dbPath)
20 | if err := os.MkdirAll(dir, 0755); err != nil {
21 | return nil, fmt.Errorf("failed to create database directory: %v", err)
22 | }
23 |
24 | // Open database connection with modernc.org/sqlite driver
25 | db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
26 | if err != nil {
27 | return nil, fmt.Errorf("failed to connect to database: %v", err)
28 | }
29 |
30 | // Initialize and run migrations
31 | m := migrations.GetMigrations(db)
32 | if err := m.Migrate(); err != nil {
33 | return nil, fmt.Errorf("failed to run migrations: %v", err)
34 | }
35 |
36 | // Close the database connection after migrations
37 | sqlDB, err := db.DB()
38 | if err != nil {
39 | return nil, fmt.Errorf("failed to get underlying database: %v", err)
40 | }
41 | if err := sqlDB.Close(); err != nil {
42 | return nil, fmt.Errorf("failed to close database after migrations: %v", err)
43 | }
44 |
45 | // Reopen the database connection for a clean state
46 | db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
47 | if err != nil {
48 | return nil, fmt.Errorf("failed to reconnect to database after migrations: %v", err)
49 | }
50 |
51 | return &DB{DB: db}, nil
52 | }
53 |
54 | // ReopenWithoutMigrations reopens the database connection without running migrations
55 | // This should be used when temporarily closing and reopening the database
56 | func ReopenWithoutMigrations(dbPath string) (*DB, error) {
57 | // Open database connection with modernc.org/sqlite driver
58 | db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
59 | if err != nil {
60 | return nil, fmt.Errorf("failed to connect to database: %v", err)
61 | }
62 |
63 | return &DB{DB: db}, nil
64 | }
65 |
66 | func (db *DB) Close() error {
67 | sqlDB, err := db.DB.DB()
68 | if err != nil {
69 | return err
70 | }
71 | return sqlDB.Close()
72 | }
73 |
--------------------------------------------------------------------------------
/internal/db/file_metadata.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // FileMetadata stores information about processed files
8 | type FileMetadata struct {
9 | ID uint `gorm:"primarykey"`
10 | JobID uint `gorm:"not null;index"`
11 | Job Job `gorm:"foreignkey:JobID"`
12 | ConfigID uint `gorm:"default:0"` // The specific config ID this file was processed with
13 | FileName string `gorm:"not null"`
14 | OriginalPath string `gorm:"not null"`
15 | FileSize int64 `gorm:"not null"`
16 | FileHash string `gorm:"index"` // MD5 or other hash for file identity
17 | CreationTime time.Time
18 | ModTime time.Time
19 | ProcessedTime time.Time `gorm:"not null"`
20 | DestinationPath string `gorm:"not null"`
21 | Status string `gorm:"not null"` // processed, archived, deleted, etc.
22 | ErrorMessage string
23 | CreatedAt time.Time
24 | UpdatedAt time.Time
25 | }
26 |
--------------------------------------------------------------------------------
/internal/db/file_metadata_store.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | // --- FileMetadata Store Methods ---
4 |
5 | // CreateFileMetadata creates a new file metadata record
6 | func (db *DB) CreateFileMetadata(metadata *FileMetadata) error {
7 | return db.Create(metadata).Error
8 | }
9 |
10 | // GetFileMetadataByJobAndName retrieves file metadata by job ID and filename
11 | func (db *DB) GetFileMetadataByJobAndName(jobID uint, fileName string) (*FileMetadata, error) {
12 | var metadata FileMetadata
13 | err := db.Where("job_id = ? AND file_name = ?", jobID, fileName).First(&metadata).Error
14 | if err != nil {
15 | return nil, err
16 | }
17 | return &metadata, nil
18 | }
19 |
20 | // GetFileMetadataByHash retrieves file metadata by file hash
21 | func (db *DB) GetFileMetadataByHash(fileHash string) (*FileMetadata, error) {
22 | var metadata FileMetadata
23 | err := db.Where("file_hash = ?", fileHash).First(&metadata).Error
24 | if err != nil {
25 | return nil, err
26 | }
27 | return &metadata, nil
28 | }
29 |
30 | // DeleteFileMetadata deletes file metadata by ID
31 | func (db *DB) DeleteFileMetadata(id uint) error {
32 | return db.Delete(&FileMetadata{}, id).Error
33 | }
34 |
--------------------------------------------------------------------------------
/internal/db/migrations/002_update_gdrive_type.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-gormigrate/gormigrate/v2"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // UpdateGDriveType updates the source_type and destination_type from 'google_drive' to 'gdrive'
14 | func UpdateGDriveType() *gormigrate.Migration {
15 | return &gormigrate.Migration{
16 | ID: "002_update_gdrive_type",
17 | Migrate: func(tx *gorm.DB) error {
18 | // Check if any tables exist (indicating an existing database)
19 | var count int64
20 | if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
21 | return fmt.Errorf("failed to check for existing tables: %v", err)
22 | }
23 |
24 | // If tables exist, create a backup
25 | if count > 0 {
26 | // Get the database path
27 | sqlDB, err := tx.DB()
28 | if err != nil {
29 | return fmt.Errorf("failed to get underlying database: %v", err)
30 | }
31 |
32 | var seq int
33 | var name, dbPath string
34 | if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
35 | return fmt.Errorf("failed to get database path: %v", err)
36 | }
37 |
38 | // Get backup directory from environment variable or use default
39 | backupDir := os.Getenv("BACKUP_DIR")
40 | if backupDir == "" {
41 | backupDir = "/app/backups" // Default Docker path
42 | // Check if we're not in Docker
43 | if _, err := os.Stat(backupDir); os.IsNotExist(err) {
44 | backupDir = "backups" // Fallback to local directory
45 | }
46 | }
47 |
48 | // Create backup directory if it doesn't exist
49 | if err := os.MkdirAll(backupDir, 0755); err != nil {
50 | return fmt.Errorf("failed to create backup directory: %v", err)
51 | }
52 |
53 | // Create backup file with timestamp in the backup directory
54 | dbFileName := filepath.Base(dbPath)
55 | backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
56 | backupFile := filepath.Join(backupDir, backupFileName)
57 |
58 | // Read original database
59 | data, err := os.ReadFile(dbPath)
60 | if err != nil {
61 | return fmt.Errorf("failed to read database for backup: %v", err)
62 | }
63 |
64 | // Write backup
65 | if err := os.WriteFile(backupFile, data, 0600); err != nil {
66 | return fmt.Errorf("failed to create database backup: %v", err)
67 | }
68 |
69 | fmt.Printf("Created database backup at: %s\n", backupFile)
70 | }
71 |
72 | // Update source_type
73 | if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'gdrive' WHERE source_type = 'google_drive'`).Error; err != nil {
74 | return err
75 | }
76 |
77 | // Update destination_type
78 | return tx.Exec(`UPDATE transfer_configs SET destination_type = 'gdrive' WHERE destination_type = 'google_drive'`).Error
79 | },
80 | Rollback: func(tx *gorm.DB) error {
81 | // Revert source_type
82 | if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'google_drive' WHERE source_type = 'gdrive'`).Error; err != nil {
83 | return err
84 | }
85 |
86 | // Revert destination_type
87 | return tx.Exec(`UPDATE transfer_configs SET destination_type = 'google_drive' WHERE destination_type = 'gdrive'`).Error
88 | },
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/internal/db/migrations/003_add_2fa.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-gormigrate/gormigrate/v2"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // Add2FA creates a migration for adding Two-Factor Authentication fields
14 | func Add2FA() *gormigrate.Migration {
15 | return &gormigrate.Migration{
16 | ID: "003_add_2fa",
17 | Migrate: func(tx *gorm.DB) error {
18 | // Check if any tables exist (indicating an existing database)
19 | var count int64
20 | if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
21 | return fmt.Errorf("failed to check for existing tables: %v", err)
22 | }
23 |
24 | // If tables exist, create a backup
25 | if count > 0 {
26 | // Get the database path
27 | sqlDB, err := tx.DB()
28 | if err != nil {
29 | return fmt.Errorf("failed to get underlying database: %v", err)
30 | }
31 |
32 | var seq int
33 | var name, dbPath string
34 | if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
35 | return fmt.Errorf("failed to get database path: %v", err)
36 | }
37 |
38 | // Get backup directory from environment variable or use default
39 | backupDir := os.Getenv("BACKUP_DIR")
40 | if backupDir == "" {
41 | backupDir = "/app/backups" // Default Docker path
42 | // Check if we're not in Docker
43 | if _, err := os.Stat(backupDir); os.IsNotExist(err) {
44 | backupDir = "backups" // Fallback to local directory
45 | }
46 | }
47 |
48 | // Create backup directory if it doesn't exist
49 | if err := os.MkdirAll(backupDir, 0755); err != nil {
50 | return fmt.Errorf("failed to create backup directory: %v", err)
51 | }
52 |
53 | // Create backup file with timestamp in the backup directory
54 | dbFileName := filepath.Base(dbPath)
55 | backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
56 | backupFile := filepath.Join(backupDir, backupFileName)
57 |
58 | // Read original database
59 | data, err := os.ReadFile(dbPath)
60 | if err != nil {
61 | return fmt.Errorf("failed to read database for backup: %v", err)
62 | }
63 |
64 | // Write backup
65 | if err := os.WriteFile(backupFile, data, 0600); err != nil {
66 | return fmt.Errorf("failed to create database backup: %v", err)
67 | }
68 |
69 | fmt.Printf("Created database backup at: %s\n", backupFile)
70 | }
71 |
72 | // Add new columns for 2FA - one at a time for SQLite compatibility
73 | if err := tx.Exec(`ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(32)`).Error; err != nil {
74 | return err
75 | }
76 | if err := tx.Exec(`ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT FALSE`).Error; err != nil {
77 | return err
78 | }
79 | if err := tx.Exec(`ALTER TABLE users ADD COLUMN backup_codes TEXT`).Error; err != nil {
80 | return err
81 | }
82 | return nil
83 | },
84 | Rollback: func(tx *gorm.DB) error {
85 | // Remove 2FA columns - one at a time for SQLite compatibility
86 | if err := tx.Exec(`ALTER TABLE users DROP COLUMN two_factor_secret`).Error; err != nil {
87 | return err
88 | }
89 | if err := tx.Exec(`ALTER TABLE users DROP COLUMN two_factor_enabled`).Error; err != nil {
90 | return err
91 | }
92 | if err := tx.Exec(`ALTER TABLE users DROP COLUMN backup_codes`).Error; err != nil {
93 | return err
94 | }
95 | return nil
96 | },
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/internal/db/migrations/008_add_user_notifications.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-gormigrate/gormigrate/v2"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // AddUserNotifications adds the user_notifications table
14 | func AddUserNotifications() *gormigrate.Migration {
15 | return &gormigrate.Migration{
16 | ID: "008_add_user_notifications",
17 | Migrate: func(tx *gorm.DB) error {
18 | // Check if any tables exist (indicating an existing database)
19 | var count int64
20 | if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
21 | return fmt.Errorf("failed to check for existing tables: %v", err)
22 | }
23 |
24 | // If tables exist, create a backup
25 | if count > 0 {
26 | // Get the database path
27 | sqlDB, err := tx.DB()
28 | if err != nil {
29 | return fmt.Errorf("failed to get underlying database: %v", err)
30 | }
31 |
32 | var seq int
33 | var name, dbPath string
34 | if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
35 | return fmt.Errorf("failed to get database path: %v", err)
36 | }
37 |
38 | // Get backup directory from environment variable or use default
39 | backupDir := os.Getenv("BACKUP_DIR")
40 | if backupDir == "" {
41 | backupDir = "/app/backups" // Default Docker path
42 | // Check if we're not in Docker
43 | if _, err := os.Stat(backupDir); os.IsNotExist(err) {
44 | backupDir = "backups" // Fallback to local directory
45 | }
46 | }
47 |
48 | // Create backup directory if it doesn't exist
49 | if err := os.MkdirAll(backupDir, 0755); err != nil {
50 | return fmt.Errorf("failed to create backup directory: %v", err)
51 | }
52 |
53 | // Create backup file with timestamp in the backup directory
54 | dbFileName := filepath.Base(dbPath)
55 | backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
56 | backupFile := filepath.Join(backupDir, backupFileName)
57 |
58 | // Read original database
59 | data, err := os.ReadFile(dbPath)
60 | if err != nil {
61 | return fmt.Errorf("failed to read database for backup: %v", err)
62 | }
63 |
64 | // Write backup
65 | if err := os.WriteFile(backupFile, data, 0600); err != nil {
66 | return fmt.Errorf("failed to create database backup: %v", err)
67 | }
68 |
69 | fmt.Printf("Created database backup at: %s\n", backupFile)
70 | }
71 | // Create the user_notifications table
72 | err := tx.Exec(`
73 | CREATE TABLE IF NOT EXISTS user_notifications (
74 | id INTEGER PRIMARY KEY AUTOINCREMENT,
75 | user_id INTEGER NOT NULL,
76 | type TEXT NOT NULL,
77 | title TEXT NOT NULL,
78 | message TEXT NOT NULL,
79 | link TEXT NOT NULL,
80 | job_id INTEGER,
81 | job_run_id INTEGER,
82 | config_id INTEGER,
83 | is_read BOOLEAN NOT NULL DEFAULT 0,
84 | created_at DATETIME NOT NULL,
85 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
86 | )
87 | `).Error
88 | if err != nil {
89 | return err
90 | }
91 |
92 | // Create an index on user_id for faster lookups
93 | err = tx.Exec(`
94 | CREATE INDEX IF NOT EXISTS idx_user_notifications_user_id ON user_notifications(user_id)
95 | `).Error
96 | if err != nil {
97 | return err
98 | }
99 |
100 | // Create an index on created_at for faster sorting
101 | err = tx.Exec(`
102 | CREATE INDEX IF NOT EXISTS idx_user_notifications_created_at ON user_notifications(created_at)
103 | `).Error
104 | if err != nil {
105 | return err
106 | }
107 |
108 | // Create an index on is_read for faster filtering of unread notifications
109 | err = tx.Exec(`
110 | CREATE INDEX IF NOT EXISTS idx_user_notifications_is_read ON user_notifications(is_read)
111 | `).Error
112 | if err != nil {
113 | return err
114 | }
115 |
116 | return nil
117 | },
118 | Rollback: func(tx *gorm.DB) error {
119 | return tx.Exec(`DROP TABLE IF EXISTS user_notifications`).Error
120 | },
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/internal/db/migrations/010_add_rclone_command_to_config.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-gormigrate/gormigrate/v2"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // AddRcloneCommandToConfig adds rclone command fields to the transfer_configs table
14 | func AddRcloneCommandToConfig() *gormigrate.Migration {
15 | return &gormigrate.Migration{
16 | ID: "010_add_rclone_command_to_config",
17 | Migrate: func(tx *gorm.DB) error {
18 | // Check if any tables exist (indicating an existing database)
19 | var count int64
20 | if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
21 | return fmt.Errorf("failed to check for existing tables: %v", err)
22 | }
23 |
24 | // If tables exist, create a backup
25 | if count > 0 {
26 | // Get the database path
27 | sqlDB, err := tx.DB()
28 | if err != nil {
29 | return fmt.Errorf("failed to get underlying database: %v", err)
30 | }
31 |
32 | var seq int
33 | var name, dbPath string
34 | if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
35 | return fmt.Errorf("failed to get database path: %v", err)
36 | }
37 |
38 | // Get backup directory from environment variable or use default
39 | backupDir := os.Getenv("BACKUP_DIR")
40 | if backupDir == "" {
41 | backupDir = "/app/backups" // Default Docker path
42 | // Check if we're not in Docker
43 | if _, err := os.Stat(backupDir); os.IsNotExist(err) {
44 | backupDir = "backups" // Fallback to local directory
45 | }
46 | }
47 |
48 | // Create backup directory if it doesn't exist
49 | if err := os.MkdirAll(backupDir, 0755); err != nil {
50 | return fmt.Errorf("failed to create backup directory: %v", err)
51 | }
52 |
53 | // Create backup file with timestamp in the backup directory
54 | dbFileName := filepath.Base(dbPath)
55 | backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
56 | backupFile := filepath.Join(backupDir, backupFileName)
57 |
58 | // Read original database
59 | data, err := os.ReadFile(dbPath)
60 | if err != nil {
61 | return fmt.Errorf("failed to read database for backup: %v", err)
62 | }
63 |
64 | // Write backup
65 | if err := os.WriteFile(backupFile, data, 0600); err != nil {
66 | return fmt.Errorf("failed to create database backup: %v", err)
67 | }
68 |
69 | fmt.Printf("Created database backup at: %s\n", backupFile)
70 | }
71 |
72 | // Add the command_id and command_flags columns to the transfer_configs table
73 | if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN command_id INTEGER DEFAULT NULL REFERENCES rclone_commands(id)`).Error; err != nil {
74 | return err
75 | }
76 |
77 | if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN command_flags TEXT DEFAULT NULL`).Error; err != nil {
78 | return err
79 | }
80 |
81 | if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN command_flag_values TEXT DEFAULT NULL`).Error; err != nil {
82 | return err
83 | }
84 |
85 | return nil
86 | },
87 | Rollback: func(tx *gorm.DB) error {
88 | // Remove the columns in reverse order
89 | if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN command_flags`).Error; err != nil {
90 | return err
91 | }
92 |
93 | if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN command_id`).Error; err != nil {
94 | return err
95 | }
96 |
97 | if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN command_flag_values`).Error; err != nil {
98 | return err
99 | }
100 |
101 | return nil
102 | },
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/internal/db/migrations/011_add_auth_providers.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-gormigrate/gormigrate/v2"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // AddAuthProviders adds tables for external authentication providers
14 | func AddAuthProviders() *gormigrate.Migration {
15 | return &gormigrate.Migration{
16 | ID: "011_add_auth_providers",
17 | Migrate: func(tx *gorm.DB) error {
18 | // Check if any tables exist (indicating an existing database)
19 | var count int64
20 | if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
21 | return fmt.Errorf("failed to check for existing tables: %v", err)
22 | }
23 |
24 | // If tables exist, create a backup
25 | if count > 0 {
26 | // Get the database path
27 | sqlDB, err := tx.DB()
28 | if err != nil {
29 | return fmt.Errorf("failed to get underlying database: %v", err)
30 | }
31 |
32 | var seq int
33 | var name, dbPath string
34 | if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
35 | return fmt.Errorf("failed to get database path: %v", err)
36 | }
37 |
38 | // Get backup directory from environment variable or use default
39 | backupDir := os.Getenv("BACKUP_DIR")
40 | if backupDir == "" {
41 | backupDir = "/app/backups" // Default Docker path
42 | // Check if we're not in Docker
43 | if _, err := os.Stat(backupDir); os.IsNotExist(err) {
44 | backupDir = "backups" // Fallback to local directory
45 | }
46 | }
47 |
48 | // Create backup directory if it doesn't exist
49 | if err := os.MkdirAll(backupDir, 0755); err != nil {
50 | return fmt.Errorf("failed to create backup directory: %v", err)
51 | }
52 |
53 | // Create backup file with timestamp in the backup directory
54 | dbFileName := filepath.Base(dbPath)
55 | backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
56 | backupFile := filepath.Join(backupDir, backupFileName)
57 |
58 | // Read original database
59 | data, err := os.ReadFile(dbPath)
60 | if err != nil {
61 | return fmt.Errorf("failed to read database for backup: %v", err)
62 | }
63 |
64 | // Write backup
65 | if err := os.WriteFile(backupFile, data, 0644); err != nil {
66 | return fmt.Errorf("failed to write database backup: %v", err)
67 | }
68 |
69 | fmt.Printf("Created database backup at %s\n", backupFile)
70 | }
71 |
72 | // Create auth_providers table
73 | if err := tx.Exec(`CREATE TABLE IF NOT EXISTS auth_providers (
74 | id INTEGER PRIMARY KEY AUTOINCREMENT,
75 | name VARCHAR(255) NOT NULL,
76 | type VARCHAR(50) NOT NULL,
77 | enabled BOOLEAN DEFAULT TRUE,
78 | description TEXT,
79 | provider_url TEXT,
80 | icon_url TEXT,
81 | client_id VARCHAR(255),
82 | client_secret VARCHAR(255),
83 | redirect_url TEXT,
84 | scopes TEXT,
85 | attribute_mapping TEXT,
86 | config TEXT,
87 | successful_logins INTEGER DEFAULT 0,
88 | last_used DATETIME,
89 | created_at DATETIME,
90 | updated_at DATETIME
91 | )`).Error; err != nil {
92 | return err
93 | }
94 |
95 | // Create external_user_identities table
96 | if err := tx.Exec(`CREATE TABLE IF NOT EXISTS external_user_identities (
97 | id INTEGER PRIMARY KEY AUTOINCREMENT,
98 | user_id INTEGER NOT NULL,
99 | provider_id INTEGER NOT NULL,
100 | provider_type VARCHAR(50) NOT NULL,
101 | external_id VARCHAR(255) NOT NULL,
102 | email VARCHAR(255) NOT NULL,
103 | username VARCHAR(255),
104 | display_name VARCHAR(255),
105 | groups TEXT,
106 | last_login DATETIME,
107 | provider_data TEXT,
108 | created_at DATETIME,
109 | updated_at DATETIME,
110 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
111 | FOREIGN KEY (provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE
112 | )`).Error; err != nil {
113 | return err
114 | }
115 |
116 | // Create unique index on provider_id and external_id
117 | if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_external_user_identities_provider_external
118 | ON external_user_identities(provider_id, external_id)`).Error; err != nil {
119 | return err
120 | }
121 |
122 | return nil
123 | },
124 | Rollback: func(tx *gorm.DB) error {
125 | if err := tx.Exec("DROP TABLE IF EXISTS external_user_identities").Error; err != nil {
126 | return err
127 | }
128 | if err := tx.Exec("DROP TABLE IF EXISTS auth_providers").Error; err != nil {
129 | return err
130 | }
131 | return nil
132 | },
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/internal/db/migrations/012a_recover_transfer_configs_rename.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // RecoverTransferConfigsRename checks for and corrects a specific inconsistent state
11 | // left by a potentially failed run of migration 012, where the transfer_configs
12 | // table might have been left renamed as _transfer_configs_old.
13 | func RecoverTransferConfigsRename() *gormigrate.Migration {
14 | return &gormigrate.Migration{
15 | ID: "012a_recover_transfer_configs_rename",
16 | Migrate: func(tx *gorm.DB) error {
17 | fmt.Println("Running migration 012a: Checking for transfer_configs rename recovery...")
18 |
19 | var oldTableExists int
20 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
21 |
22 | var newTableExists int
23 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='transfer_configs'").Scan(&newTableExists)
24 |
25 | if oldTableExists > 0 && newTableExists == 0 {
26 | fmt.Println("Found _transfer_configs_old table but not transfer_configs. Attempting recovery rename...")
27 | if err := tx.Exec("ALTER TABLE _transfer_configs_old RENAME TO transfer_configs").Error; err != nil {
28 | return fmt.Errorf("failed to rename _transfer_configs_old back to transfer_configs: %w", err)
29 | }
30 | fmt.Println("Successfully renamed _transfer_configs_old to transfer_configs.")
31 | } else if oldTableExists > 0 && newTableExists > 0 {
32 | // This state shouldn't ideally happen if migration 012 followed its logic,
33 | // but indicates a potential issue. Maybe drop the old one? For now, just log.
34 | fmt.Println("Warning: Both transfer_configs and _transfer_configs_old tables exist. Manual inspection might be needed.")
35 | } else {
36 | fmt.Println("No recovery needed for transfer_configs rename.")
37 | }
38 |
39 | return nil
40 | },
41 | Rollback: func(tx *gorm.DB) error {
42 | // Rollback doesn't make sense for a recovery step.
43 | fmt.Println("Rollback for migration 012a_recover_transfer_configs_rename is not applicable.")
44 | return nil
45 | },
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/internal/db/migrations/012b_recover_notification_services_rename.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // RecoverNotificationServicesRename checks for and corrects a specific inconsistent state
11 | // left by a potentially failed run of migration 012, where the notification_services
12 | // table might have been left renamed as _notification_services_old.
13 | func RecoverNotificationServicesRename() *gormigrate.Migration {
14 | return &gormigrate.Migration{
15 | ID: "012b_recover_notification_services_rename",
16 | Migrate: func(tx *gorm.DB) error {
17 | fmt.Println("Running migration 012b: Checking for notification_services rename recovery...")
18 |
19 | var oldTableExists int
20 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_notification_services_old'").Scan(&oldTableExists)
21 |
22 | var newTableExists int
23 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='notification_services'").Scan(&newTableExists)
24 |
25 | if oldTableExists > 0 && newTableExists == 0 {
26 | fmt.Println("Found _notification_services_old table but not notification_services. Attempting recovery rename...")
27 | if err := tx.Exec("ALTER TABLE _notification_services_old RENAME TO notification_services").Error; err != nil {
28 | return fmt.Errorf("failed to rename _notification_services_old back to notification_services: %w", err)
29 | }
30 | fmt.Println("Successfully renamed _notification_services_old to notification_services.")
31 | } else if oldTableExists > 0 && newTableExists > 0 {
32 | fmt.Println("Warning: Both notification_services and _notification_services_old tables exist. Manual inspection might be needed.")
33 | } else {
34 | fmt.Println("No recovery needed for notification_services rename.")
35 | }
36 |
37 | return nil
38 | },
39 | Rollback: func(tx *gorm.DB) error {
40 | // Rollback doesn't make sense for a recovery step.
41 | fmt.Println("Rollback for migration 012b_recover_notification_services_rename is not applicable.")
42 | return nil
43 | },
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/internal/db/migrations/012c_recover_auth_providers_rename.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // RecoverAuthProvidersRename checks for and corrects a specific inconsistent state
11 | // left by a potentially failed run of migration 012, where the auth_providers
12 | // table might have been left renamed as _auth_providers_old.
13 | func RecoverAuthProvidersRename() *gormigrate.Migration {
14 | return &gormigrate.Migration{
15 | ID: "012c_recover_auth_providers_rename",
16 | Migrate: func(tx *gorm.DB) error {
17 | fmt.Println("Running migration 012c: Checking for auth_providers rename recovery...")
18 |
19 | var oldTableExists int
20 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_auth_providers_old'").Scan(&oldTableExists)
21 |
22 | var newTableExists int
23 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='auth_providers'").Scan(&newTableExists)
24 |
25 | if oldTableExists > 0 && newTableExists == 0 {
26 | fmt.Println("Found _auth_providers_old table but not auth_providers. Attempting recovery rename...")
27 | if err := tx.Exec("ALTER TABLE _auth_providers_old RENAME TO auth_providers").Error; err != nil {
28 | return fmt.Errorf("failed to rename _auth_providers_old back to auth_providers: %w", err)
29 | }
30 | fmt.Println("Successfully renamed _auth_providers_old to auth_providers.")
31 | } else if oldTableExists > 0 && newTableExists > 0 {
32 | fmt.Println("Warning: Both auth_providers and _auth_providers_old tables exist. Manual inspection might be needed.")
33 | } else {
34 | fmt.Println("No recovery needed for auth_providers rename.")
35 | }
36 |
37 | return nil
38 | },
39 | Rollback: func(tx *gorm.DB) error {
40 | // Rollback doesn't make sense for a recovery step.
41 | fmt.Println("Rollback for migration 012c_recover_auth_providers_rename is not applicable.")
42 | return nil
43 | },
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/internal/db/migrations/013_cleanup_invalid_booleans.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // CleanupInvalidBooleans updates boolean columns represented as integers
11 | // to ensure they only contain valid values (0, 1, or NULL).
12 | func CleanupInvalidBooleans() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "013_cleanup_invalid_booleans",
15 | Migrate: func(tx *gorm.DB) error {
16 | fmt.Println("Running migration 013: Cleaning up invalid boolean values...")
17 |
18 | // Target: transfer_configs.delete_after_transfer
19 | // Set any non-NULL value that is not 0 or 1 to 0 (false)
20 | sql := `UPDATE transfer_configs
21 | SET delete_after_transfer = 0
22 | WHERE delete_after_transfer IS NOT NULL AND delete_after_transfer NOT IN (0, 1);`
23 |
24 | if err := tx.Exec(sql).Error; err != nil {
25 | return fmt.Errorf("failed to cleanup delete_after_transfer in transfer_configs: %w", err)
26 | }
27 | fmt.Println("Cleaned up invalid values in transfer_configs.delete_after_transfer.")
28 |
29 | // Target: transfer_configs.archive_enabled
30 | sql = `UPDATE transfer_configs
31 | SET archive_enabled = 0
32 | WHERE archive_enabled IS NOT NULL AND archive_enabled NOT IN (0, 1);`
33 | if err := tx.Exec(sql).Error; err != nil {
34 | return fmt.Errorf("failed to cleanup archive_enabled in transfer_configs: %w", err)
35 | }
36 | fmt.Println("Cleaned up invalid values in transfer_configs.archive_enabled.")
37 |
38 | // Target: transfer_configs.skip_processed_files
39 | sql = `UPDATE transfer_configs
40 | SET skip_processed_files = 0
41 | WHERE skip_processed_files IS NOT NULL AND skip_processed_files NOT IN (0, 1);`
42 | if err := tx.Exec(sql).Error; err != nil {
43 | return fmt.Errorf("failed to cleanup skip_processed_files in transfer_configs: %w", err)
44 | }
45 | fmt.Println("Cleaned up invalid values in transfer_configs.skip_processed_files.")
46 |
47 | // Target: notification_services.is_enabled
48 | sql = `UPDATE notification_services
49 | SET is_enabled = 0
50 | WHERE is_enabled IS NOT NULL AND is_enabled NOT IN (0, 1);`
51 | if err := tx.Exec(sql).Error; err != nil {
52 | // Check if the table exists before failing hard
53 | var tableExists int
54 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='notification_services'").Scan(&tableExists)
55 | if tableExists == 0 {
56 | fmt.Println("Skipping cleanup for notification_services.is_enabled: table does not exist.")
57 | } else {
58 | return fmt.Errorf("failed to cleanup is_enabled in notification_services: %w", err)
59 | }
60 | } else {
61 | fmt.Println("Cleaned up invalid values in notification_services.is_enabled.")
62 | }
63 |
64 | // Target: auth_providers.enabled
65 | sql = `UPDATE auth_providers
66 | SET enabled = 0
67 | WHERE enabled IS NOT NULL AND enabled NOT IN (0, 1);`
68 | if err := tx.Exec(sql).Error; err != nil {
69 | // Check if the table exists before failing hard
70 | var tableExists int
71 | tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='auth_providers'").Scan(&tableExists)
72 | if tableExists == 0 {
73 | fmt.Println("Skipping cleanup for auth_providers.enabled: table does not exist.")
74 | } else {
75 | return fmt.Errorf("failed to cleanup enabled in auth_providers: %w", err)
76 | }
77 | } else {
78 | fmt.Println("Cleaned up invalid values in auth_providers.enabled.")
79 | }
80 |
81 | fmt.Println("Migration 013 completed successfully.")
82 | return nil
83 | },
84 | Rollback: func(tx *gorm.DB) error {
85 | // This migration cleans up data. Rolling back doesn't make sense
86 | // as we don't know the original invalid values.
87 | fmt.Println("Rollback for migration 013_cleanup_invalid_booleans is not applicable.")
88 | return nil
89 | },
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/internal/db/migrations/migrations.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | var migrations []*gormigrate.Migration
9 |
10 | // GetMigrations returns all migrations
11 | func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
12 | // Add all migrations in order
13 | migrations = append(migrations,
14 | InitialSchema(), // 001
15 | UpdateGDriveType(), // 002
16 | Add2FA(), // 003
17 | AddAuditLogs(), // 004
18 | AddDefaultRoles(), // 005
19 | AddTimestampsToJobHistories(), // 006
20 | AddNotificationServices(), // 007
21 | AddUserNotifications(), // 008
22 | AddRcloneTables(), // 009
23 | AddRcloneCommandToConfig(), // 010
24 | AddAuthProviders(), // 011
25 | AlterBooleanDefaults(), // 012
26 | RecoverTransferConfigsRename(), // 012a
27 | RecoverNotificationServicesRename(), // 012b
28 | RecoverAuthProvidersRename(), // 012c
29 | CleanupInvalidBooleans(), // 013
30 | )
31 |
32 | return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
33 | }
34 |
--------------------------------------------------------------------------------
/internal/db/notification.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | // NotificationService represents a notification service configuration
11 | type NotificationService struct {
12 | ID uint `json:"id" gorm:"primaryKey"`
13 | Name string `json:"name" gorm:"not null"`
14 | Type string `json:"type" gorm:"not null"` // email, webhook
15 | IsEnabled *bool `json:"is_enabled" gorm:"default:true"`
16 | Config map[string]string `json:"config" gorm:"-"`
17 | ConfigJSON string `json:"-" gorm:"column:config"`
18 | Description string `json:"description"`
19 | EventTriggers []string `json:"event_triggers" gorm:"-"`
20 | EventTriggersJSON string `json:"-" gorm:"column:event_triggers;default:'[]'"`
21 | PayloadTemplate string `json:"payload_template" gorm:"column:payload_template"`
22 | SecretKey string `json:"secret_key" gorm:"column:secret_key"`
23 | RetryPolicy string `json:"retry_policy" gorm:"column:retry_policy;default:'simple'"`
24 | LastUsed time.Time `json:"last_used" gorm:"column:last_used"`
25 | SuccessCount int `json:"success_count" gorm:"column:success_count;default:0"`
26 | FailureCount int `json:"failure_count" gorm:"column:failure_count;default:0"`
27 | CreatedBy uint `json:"created_by"`
28 | CreatedAt time.Time `json:"created_at"`
29 | UpdatedAt time.Time `json:"updated_at"`
30 | }
31 |
32 | // BeforeSave converts Config map and EventTriggers to JSON strings for storage
33 | func (n *NotificationService) BeforeSave(tx *gorm.DB) error {
34 | configJSON, err := json.Marshal(n.Config)
35 | if err != nil {
36 | return err
37 | }
38 | n.ConfigJSON = string(configJSON)
39 |
40 | eventsJSON, err := json.Marshal(n.EventTriggers)
41 | if err != nil {
42 | return err
43 | }
44 | n.EventTriggersJSON = string(eventsJSON)
45 |
46 | return nil
47 | }
48 |
49 | // AfterFind converts JSON strings back to Config map and EventTriggers
50 | func (n *NotificationService) AfterFind(tx *gorm.DB) error {
51 | if n.ConfigJSON != "" {
52 | if err := json.Unmarshal([]byte(n.ConfigJSON), &n.Config); err != nil {
53 | return err
54 | }
55 | }
56 |
57 | if n.EventTriggersJSON != "" {
58 | if err := json.Unmarshal([]byte(n.EventTriggersJSON), &n.EventTriggers); err != nil {
59 | return err
60 | }
61 | }
62 |
63 | return nil
64 | }
65 |
66 | // GetNotificationServices returns notification services, filtered by enabled status if specified
67 | func (db *DB) GetNotificationServices(onlyEnabled bool) ([]NotificationService, error) {
68 | var services []NotificationService
69 | query := db.DB
70 |
71 | if onlyEnabled {
72 | // When using a pointer, we need to explicitly check for true
73 | // GORM handles the underlying SQL correctly for different dialects
74 | query = query.Where("is_enabled = ?", true)
75 | }
76 |
77 | if err := query.Find(&services).Error; err != nil {
78 | return nil, err
79 | }
80 |
81 | return services, nil
82 | }
83 |
84 | // GetNotificationService returns a notification service by ID
85 | func (db *DB) GetNotificationService(id uint) (*NotificationService, error) {
86 | var service NotificationService
87 | if err := db.First(&service, id).Error; err != nil {
88 | return nil, err
89 | }
90 | return &service, nil
91 | }
92 |
93 | // CreateNotificationService creates a new notification service
94 | func (db *DB) CreateNotificationService(service *NotificationService) error {
95 | return db.Create(service).Error
96 | }
97 |
98 | // UpdateNotificationService updates an existing notification service
99 | func (db *DB) UpdateNotificationService(service *NotificationService) error {
100 | return db.Save(service).Error
101 | }
102 |
103 | // DeleteNotificationService deletes a notification service by ID
104 | func (db *DB) DeleteNotificationService(id uint) error {
105 | return db.Delete(&NotificationService{}, id).Error
106 | }
107 |
108 | // --- NotificationService Helper Methods ---
109 |
110 | // GetIsEnabled returns the value of IsEnabled with a default if nil
111 | func (n *NotificationService) GetIsEnabled() bool {
112 | if n.IsEnabled == nil {
113 | // If the pointer is nil, GORM might not have set it,
114 | // or it was explicitly set to nil. We assume the DB default (true)
115 | // if it's nil, aligning with the original gorm tag default.
116 | return true
117 | }
118 | return *n.IsEnabled
119 | }
120 |
121 | // SetIsEnabled sets the IsEnabled field
122 | func (n *NotificationService) SetIsEnabled(value bool) {
123 | n.IsEnabled = &value
124 | }
125 |
--------------------------------------------------------------------------------
/internal/db/rclone.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 | )
8 |
9 | // RcloneCommand represents a command available in rclone
10 | type RcloneCommand struct {
11 | ID uint `gorm:"primarykey"`
12 | Name string `gorm:"not null;uniqueIndex"`
13 | Description string `gorm:"not null"`
14 | Category string `gorm:"not null;index"`
15 | IsAdvanced bool `gorm:"not null;default:false"`
16 | Flags []RcloneCommandFlag `gorm:"foreignKey:CommandID;constraint:OnDelete:CASCADE"`
17 | CreatedAt time.Time `gorm:"not null"`
18 | }
19 |
20 | // RcloneCommandFlag represents a flag that can be used with an rclone command
21 | type RcloneCommandFlag struct {
22 | ID uint `gorm:"primarykey"`
23 | CommandID uint `gorm:"not null;index"`
24 | Command RcloneCommand `gorm:"foreignKey:CommandID"`
25 | Name string `gorm:"not null;index"`
26 | ShortName string
27 | Description string `gorm:"not null"`
28 | DataType string `gorm:"not null"` // string, int, bool, etc.
29 | IsRequired bool `gorm:"not null;default:false"`
30 | DefaultValue string
31 | CreatedAt time.Time `gorm:"not null"`
32 | }
33 |
34 | // --- Rclone Helper Methods ---
35 |
36 | // GetUsageExample returns a human-readable usage example for a flag
37 | func (flag *RcloneCommandFlag) GetUsageExample() string {
38 | switch flag.DataType {
39 | case "bool":
40 | return flag.Name
41 | case "int":
42 | return fmt.Sprintf("%s=", flag.Name)
43 | case "float":
44 | return fmt.Sprintf("%s=", flag.Name)
45 | case "string":
46 | return fmt.Sprintf("%s=", flag.Name)
47 | default:
48 | return fmt.Sprintf("%s=", flag.Name)
49 | }
50 | }
51 |
52 | // ParseRcloneFlags parses a string of rclone flags into a map
53 | // Note: This is a general utility function, not tied to a specific struct instance.
54 | // It might be better placed in a more general utility package if one exists,
55 | // but keeping it here for now as per the original file structure.
56 | func ParseRcloneFlags(flagsStr string) map[string]string {
57 | result := make(map[string]string)
58 | if flagsStr == "" {
59 | return result
60 | }
61 |
62 | // Split the flags string by spaces
63 | parts := strings.Fields(flagsStr)
64 |
65 | for i := 0; i < len(parts); i++ {
66 | part := parts[i]
67 |
68 | // Check if it's a flag (starts with --)
69 | if strings.HasPrefix(part, "--") {
70 | // Remove the -- prefix
71 | flagName := part // Keep the '--' prefix in the map key for consistency? Or remove? Plan used remove.
72 | // flagName := strings.TrimPrefix(part, "--") // Alternative: remove prefix
73 |
74 | // Check if the flag has a value
75 | if i+1 < len(parts) && !strings.HasPrefix(parts[i+1], "--") {
76 | // Next part is a value
77 | result[flagName] = parts[i+1]
78 | i++ // Skip the value in the next iteration
79 | } else {
80 | // Flag without value, treat as boolean true
81 | result[flagName] = "true"
82 | }
83 | }
84 | }
85 |
86 | return result
87 | }
88 |
--------------------------------------------------------------------------------
/internal/db/role_store.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | // --- Role Store Methods ---
11 |
12 | // CreateRole creates a new role record
13 | func (db *DB) CreateRole(role *Role) error {
14 | return db.Create(role).Error
15 | }
16 |
17 | // GetRole retrieves a role by ID, preloading permissions
18 | func (db *DB) GetRole(id uint) (*Role, error) {
19 | var role Role
20 | // Assuming Permissions are handled correctly by GORM or custom type
21 | err := db.First(&role, id).Error
22 | if err != nil {
23 | return nil, err
24 | }
25 | return &role, nil
26 | }
27 |
28 | // GetRoleByName retrieves a role by name, preloading permissions
29 | func (db *DB) GetRoleByName(name string) (*Role, error) {
30 | var role Role
31 | err := db.Where("name = ?", name).First(&role).Error
32 | if err != nil {
33 | return nil, err
34 | }
35 | return &role, nil
36 | }
37 |
38 | // UpdateRole updates an existing role record
39 | func (db *DB) UpdateRole(role *Role) error {
40 | // Use Omit Users to prevent GORM from trying to update the many2many relationship directly here
41 | return db.Omit("Users").Save(role).Error
42 | }
43 |
44 | // DeleteRole deletes a role after checking dependencies and removing assignments
45 | func (db *DB) DeleteRole(id uint) error {
46 | var role Role
47 | if err := db.First(&role, id).Error; err != nil {
48 | return fmt.Errorf("role not found: %w", err)
49 | }
50 |
51 | if role.IsSystemRole() {
52 | return errors.New("cannot delete system role")
53 | }
54 |
55 | // Start transaction
56 | tx := db.Begin()
57 | if err := tx.Error; err != nil {
58 | return err
59 | }
60 |
61 | // Manually delete role assignments from the join table
62 | if err := tx.Exec("DELETE FROM user_roles WHERE role_id = ?", id).Error; err != nil {
63 | tx.Rollback()
64 | return fmt.Errorf("failed to delete role assignments: %w", err)
65 | }
66 |
67 | // Delete the role itself
68 | if err := tx.Delete(&role).Error; err != nil {
69 | tx.Rollback()
70 | return fmt.Errorf("failed to delete role: %w", err)
71 | }
72 |
73 | // Commit transaction
74 | return tx.Commit().Error
75 | }
76 |
77 | // ListRoles retrieves all roles
78 | func (db *DB) ListRoles() ([]Role, error) {
79 | var roles []Role
80 | err := db.Find(&roles).Error
81 | return roles, err
82 | }
83 |
84 | // GetUserRoles retrieves all roles assigned to a specific user ID
85 | func (db *DB) GetUserRoles(userID uint) ([]Role, error) {
86 | var user User
87 | // Preload the Roles association
88 | if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
89 | // Handle case where user might not be found vs. other errors
90 | if errors.Is(err, gorm.ErrRecordNotFound) {
91 | return nil, fmt.Errorf("user with ID %d not found", userID)
92 | }
93 | return nil, fmt.Errorf("failed to get user %d roles: %w", userID, err)
94 | }
95 | return user.Roles, nil
96 | }
97 |
98 | // AssignRoleToUser assigns a role to a user, handling the join table
99 | func (db *DB) AssignRoleToUser(roleID, userID, assignedByID uint) error {
100 | var role Role
101 | if err := db.First(&role, roleID).Error; err != nil {
102 | return fmt.Errorf("role with ID %d not found: %w", roleID, err)
103 | }
104 | var user User
105 | if err := db.First(&user, userID).Error; err != nil {
106 | return fmt.Errorf("user with ID %d not found: %w", userID, err)
107 | }
108 |
109 | // Use GORM's Association API for many2many
110 | err := db.Model(&user).Association("Roles").Append(&role)
111 | if err != nil {
112 | return fmt.Errorf("failed to assign role %d to user %d: %w", roleID, userID, err)
113 | }
114 |
115 | // Optionally, log the assignment (consider moving audit logging to a dedicated service/hook)
116 | // db.Create(&AuditLog{...})
117 |
118 | return nil
119 | }
120 |
121 | // UnassignRoleFromUser removes a role from a user, handling the join table
122 | func (db *DB) UnassignRoleFromUser(roleID, userID, unassignedByID uint) error {
123 | var role Role
124 | if err := db.First(&role, roleID).Error; err != nil {
125 | return fmt.Errorf("role with ID %d not found: %w", roleID, err)
126 | }
127 | var user User
128 | // Need to preload roles to check if the association exists before deleting
129 | if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
130 | return fmt.Errorf("user with ID %d not found: %w", userID, err)
131 | }
132 |
133 | // Use GORM's Association API for many2many deletion
134 | err := db.Model(&user).Association("Roles").Delete(&role)
135 | if err != nil {
136 | return fmt.Errorf("failed to unassign role %d from user %d: %w", roleID, userID, err)
137 | }
138 |
139 | // Optionally, log the unassignment
140 | // db.Create(&AuditLog{...})
141 |
142 | return nil
143 | }
144 |
--------------------------------------------------------------------------------
/internal/db/user_store.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // --- User Store Methods ---
8 |
9 | // CreateUser creates a new user record
10 | func (db *DB) CreateUser(user *User) error {
11 | return db.Create(user).Error
12 | }
13 |
14 | // GetUserByEmail retrieves a user by their email address
15 | func (db *DB) GetUserByEmail(email string) (*User, error) {
16 | var user User
17 | // Preload Roles to ensure they are available for permission checks
18 | err := db.Preload("Roles").Where("email = ?", email).First(&user).Error
19 | if err != nil {
20 | return nil, err
21 | }
22 | return &user, nil
23 | }
24 |
25 | // GetUserByID retrieves a user by their ID
26 | func (db *DB) GetUserByID(id uint) (*User, error) {
27 | var user User
28 | // Preload Roles
29 | err := db.Preload("Roles").First(&user, id).Error
30 | if err != nil {
31 | return nil, err
32 | }
33 | return &user, nil
34 | }
35 |
36 | // UpdateUser updates an existing user record
37 | func (db *DB) UpdateUser(user *User) error {
38 | // Use Omit to prevent accidentally changing Roles association directly
39 | // Role assignments should use AssignRole/UnassignRole methods
40 | return db.Omit("Roles").Save(user).Error
41 | }
42 |
43 | // --- PasswordResetToken Store Methods ---
44 |
45 | // CreatePasswordResetToken creates a new password reset token record
46 | func (db *DB) CreatePasswordResetToken(token *PasswordResetToken) error {
47 | return db.Create(token).Error
48 | }
49 |
50 | // GetPasswordResetToken retrieves a valid, unused password reset token
51 | func (db *DB) GetPasswordResetToken(token string) (*PasswordResetToken, error) {
52 | var resetToken PasswordResetToken
53 | err := db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&resetToken).Error
54 | if err != nil {
55 | return nil, err
56 | }
57 | return &resetToken, nil
58 | }
59 |
60 | // MarkPasswordResetTokenAsUsed marks a password reset token as used
61 | func (db *DB) MarkPasswordResetTokenAsUsed(tokenID uint) error {
62 | return db.Model(&PasswordResetToken{}).Where("id = ?", tokenID).Update("used", true).Error
63 | }
64 |
--------------------------------------------------------------------------------
/internal/email/mock_email.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/starfleetcptn/gomft/internal/config"
7 | )
8 |
9 | // MockService implements the email Service for testing purposes
10 | type MockService struct {
11 | SendEmailCalls int
12 | SendPasswordResetEmailCalls int
13 | ReturnError error
14 | }
15 |
16 | // NewMockService creates a new mock email service
17 | func NewMockService() *Service {
18 | // Create minimal config
19 | cfg := &config.Config{
20 | Email: config.EmailConfig{
21 | Enabled: false,
22 | },
23 | BaseURL: "http://localhost:8080",
24 | }
25 |
26 | return &Service{
27 | Config: cfg,
28 | }
29 | }
30 |
31 | // SendPasswordResetEmail mocks sending a password reset email
32 | func (s *MockService) SendPasswordResetEmail(toEmail, username, resetToken string) error {
33 | return fmt.Errorf("email service is disabled, reset link would be: %s/reset-password?token=%s",
34 | "http://localhost:8080", resetToken)
35 | }
36 |
--------------------------------------------------------------------------------
/internal/scheduler/metadata.go:
--------------------------------------------------------------------------------
1 | package scheduler
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/starfleetcptn/gomft/internal/db"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // MetadataDB defines the database methods needed by MetadataHandler.
12 | // This allows for easier mocking during testing.
13 | type MetadataDB interface {
14 | GetFileMetadataByHash(hash string) (*db.FileMetadata, error)
15 | GetFileMetadataByJobAndName(jobID uint, fileName string) (*db.FileMetadata, error)
16 | }
17 |
18 | // MetadataHandler handles checking file processing history.
19 | type MetadataHandler struct {
20 | db MetadataDB // Use the interface type
21 | logger *Logger // Added logger dependency
22 | }
23 |
24 | // NewMetadataHandler creates a new MetadataHandler.
25 | func NewMetadataHandler(database MetadataDB, logger *Logger) *MetadataHandler { // Accept the interface type
26 | return &MetadataHandler{
27 | db: database,
28 | logger: logger,
29 | }
30 | }
31 |
32 | // hasFileBeenProcessed checks if a file with the same hash has been processed before.
33 | func (mh *MetadataHandler) hasFileBeenProcessed(jobID uint, fileHash string) (bool, *db.FileMetadata, error) {
34 | if fileHash == "" {
35 | return false, nil, nil
36 | }
37 |
38 | // First try to find by hash (most reliable)
39 | metadata, err := mh.db.GetFileMetadataByHash(fileHash) // Calls the interface method
40 | if err == nil && metadata != nil {
41 | // Optional: Add logging here if needed
42 | mh.logger.LogDebug("Found existing metadata by hash for job %d, hash %s", jobID, fileHash)
43 | return true, metadata, nil
44 | }
45 | // Handle DB errors
46 | if err != nil {
47 | // If the error is specifically "record not found", it means not processed, which is not an error for this function.
48 | if errors.Is(err, gorm.ErrRecordNotFound) {
49 | return false, nil, nil // Not found, no error to return
50 | }
51 | // For any other DB error, log it and return it.
52 | mh.logger.LogError("Error checking metadata by hash for job %d, hash %s: %v", jobID, fileHash, err)
53 | return false, nil, err // Return the actual DB error
54 | }
55 |
56 | // Should not be reached if err is nil and metadata is nil, but return false just in case.
57 | return false, nil, nil
58 | }
59 |
60 | // checkFileProcessingHistory checks processing history for a given file name within a specific job.
61 | func (mh *MetadataHandler) checkFileProcessingHistory(jobID uint, fileName string) (*db.FileMetadata, error) {
62 | // Try to find by job and filename
63 | metadata, err := mh.db.GetFileMetadataByJobAndName(jobID, fileName) // Calls the interface method
64 | if err == nil && metadata != nil {
65 | mh.logger.LogDebug("Found existing metadata by name for job %d, file %s", jobID, fileName)
66 | return metadata, nil
67 | }
68 |
69 | if err != nil {
70 | mh.logger.LogError("Error checking metadata by name for job %d, file %s: %v", jobID, fileName, err)
71 | // Don't return error here, just indicate not found
72 | }
73 |
74 | return nil, fmt.Errorf("no history found for file %s in job %d", fileName, jobID)
75 | }
76 |
--------------------------------------------------------------------------------
/internal/scheduler/mock_scheduler.go:
--------------------------------------------------------------------------------
1 | package scheduler
2 |
3 | import (
4 | "github.com/starfleetcptn/gomft/internal/db"
5 | )
6 |
7 | // MockScheduler is a mock implementation of a scheduler for testing
8 | type MockScheduler struct {
9 | ScheduledJobs map[uint]bool
10 | UnscheduledJobs map[uint]bool
11 | RunJobsNow map[uint]bool
12 | ScheduleJobErr error
13 | RunJobNowErr error
14 | UnscheduleJobCalls int
15 | MultiConfigJobs map[uint][]uint // Track jobs with multiple configs (job ID -> config IDs)
16 | }
17 |
18 | // NewMockScheduler creates a new mock scheduler
19 | func NewMockScheduler() *MockScheduler {
20 | return &MockScheduler{
21 | ScheduledJobs: make(map[uint]bool),
22 | UnscheduledJobs: make(map[uint]bool),
23 | RunJobsNow: make(map[uint]bool),
24 | MultiConfigJobs: make(map[uint][]uint),
25 | }
26 | }
27 |
28 | // ScheduleJob mocks scheduling a job
29 | func (m *MockScheduler) ScheduleJob(job *db.Job) error {
30 | if m.ScheduleJobErr != nil {
31 | return m.ScheduleJobErr
32 | }
33 |
34 | if job.GetEnabled() {
35 | m.ScheduledJobs[job.ID] = true
36 | delete(m.UnscheduledJobs, job.ID)
37 | } else {
38 | m.UnscheduledJobs[job.ID] = true
39 | delete(m.ScheduledJobs, job.ID)
40 | }
41 |
42 | // Track jobs with multiple configurations
43 | if job.ConfigIDs != "" {
44 | m.MultiConfigJobs[job.ID] = job.GetConfigIDsList()
45 | }
46 |
47 | return nil
48 | }
49 |
50 | // RunJobNow mocks running a job immediately
51 | func (m *MockScheduler) RunJobNow(jobID uint) error {
52 | if m.RunJobNowErr != nil {
53 | return m.RunJobNowErr
54 | }
55 |
56 | m.RunJobsNow[jobID] = true
57 |
58 | // In a real implementation, this would execute the job
59 | // But for testing, we just record that it was called
60 | return nil
61 | }
62 |
63 | // UnscheduleJob mocks unscheduling a job
64 | func (m *MockScheduler) UnscheduleJob(jobID uint) {
65 | m.UnscheduleJobCalls++
66 | m.UnscheduledJobs[jobID] = true
67 | delete(m.ScheduledJobs, jobID)
68 | delete(m.MultiConfigJobs, jobID)
69 | }
70 |
71 | // Stop mocks stopping the scheduler
72 | func (m *MockScheduler) Stop() {
73 | // Nothing to do
74 | }
75 |
76 | // RotateLogs mocks log rotation
77 | func (m *MockScheduler) RotateLogs() error {
78 | return nil
79 | }
80 |
81 | // IsJobWithMultipleConfigs checks if a job is scheduled with multiple configs
82 | func (m *MockScheduler) IsJobWithMultipleConfigs(jobID uint) bool {
83 | configs, exists := m.MultiConfigJobs[jobID]
84 | return exists && len(configs) > 1
85 | }
86 |
87 | // GetConfigsForJob returns the configs for a job
88 | func (m *MockScheduler) GetConfigsForJob(jobID uint) []uint {
89 | return m.MultiConfigJobs[jobID]
90 | }
91 |
--------------------------------------------------------------------------------
/internal/scheduler/scheduler_interface.go:
--------------------------------------------------------------------------------
1 | package scheduler
2 |
3 | import (
4 | "github.com/starfleetcptn/gomft/internal/db"
5 | )
6 |
7 | // SchedulerInterface defines the interface for job scheduling operations
8 | type SchedulerInterface interface {
9 | // ScheduleJob schedules a job based on its cron expression
10 | ScheduleJob(job *db.Job) error
11 |
12 | // RunJobNow runs a job immediately
13 | RunJobNow(jobID uint) error
14 |
15 | // UnscheduleJob removes a job from the scheduler
16 | UnscheduleJob(jobID uint)
17 |
18 | // Stop stops the scheduler
19 | Stop()
20 | }
21 |
--------------------------------------------------------------------------------
/internal/scheduler/utils.go:
--------------------------------------------------------------------------------
1 | package scheduler
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "path/filepath"
7 | "regexp"
8 | "strings"
9 | "time"
10 | )
11 |
12 | // ProcessOutputPattern processes an output pattern with variables and returns the result
13 | // This function is useful for testing pattern processing in isolation
14 | func ProcessOutputPattern(pattern string, originalFilename string) string {
15 | // Process date variables
16 | dateRegex := regexp.MustCompile(`\${date:([^}]+)}`)
17 | processedPattern := dateRegex.ReplaceAllStringFunc(pattern, func(match string) string {
18 | format := dateRegex.FindStringSubmatch(match)[1]
19 | return time.Now().Format(format)
20 | })
21 |
22 | // Split the filename and extension
23 | ext := filepath.Ext(originalFilename)
24 | filename := strings.TrimSuffix(originalFilename, ext)
25 |
26 | // Replace filename and extension variables
27 | processedPattern = strings.ReplaceAll(processedPattern, "${filename}", filename)
28 | // Remove leading dot from ext before replacing
29 | processedPattern = strings.ReplaceAll(processedPattern, "${ext}", strings.TrimPrefix(ext, "."))
30 |
31 | return processedPattern
32 | }
33 |
34 | // createRcloneFilterFile creates a temporary filter file for rclone with rename rules
35 | func createRcloneFilterFile(pattern string) (string, error) {
36 | // Create a temporary file
37 | tmpFile, err := ioutil.TempFile("", "rclone-filter-*.txt")
38 | if err != nil {
39 | return "", fmt.Errorf("failed to create temporary filter file: %v", err)
40 | }
41 | defer tmpFile.Close()
42 |
43 | // Process the pattern to create a rclone filter rule
44 | // First, replace date variables with current date in the specified format
45 | dateRegex := regexp.MustCompile(`\${date:([^}]+)}`)
46 | processedPattern := dateRegex.ReplaceAllStringFunc(pattern, func(match string) string {
47 | format := dateRegex.FindStringSubmatch(match)[1]
48 | return time.Now().Format(format)
49 | })
50 |
51 | // Replace filename and extension variables with rclone's capture group references
52 | // For rclone rename filters, we need to use {1} for the first capture group, not $1
53 | // See: https://rclone.org/filtering/#rename
54 |
55 | // Extract filename without extension
56 | processedPattern = strings.ReplaceAll(processedPattern, "${filename}", "{1}")
57 |
58 | // Extract extension (with the dot)
59 | processedPattern = strings.ReplaceAll(processedPattern, "${ext}", "{2}")
60 | // Create a rename rule for rclone using the correct syntax:
61 | // - The format for rename filters is: "-- SourceRegexp ReplacementPattern"
62 | // - For files with extension: capture the name and extension separately
63 | rule := fmt.Sprintf("-- (.*)(\\..+)$ %s\n", processedPattern) // Correct escaping for dot
64 |
65 | // Add a fallback rule for files without extension
66 | // Keep [^.] as it correctly excludes literal dot in character class
67 | fallbackRule := fmt.Sprintf("-- ([^.]+)$ %s\n",
68 | strings.ReplaceAll(processedPattern, "{2}", ""))
69 | // Removed duplicate declaration below
70 |
71 | // Write the rules to the file
72 | if _, err := tmpFile.WriteString(rule + fallbackRule); err != nil {
73 | return "", fmt.Errorf("failed to write to filter file: %v", err)
74 | }
75 |
76 | return tmpFile.Name(), nil
77 | }
78 |
--------------------------------------------------------------------------------
/internal/testutils/testutils.go:
--------------------------------------------------------------------------------
1 | // Package testutils provides utilities for testing the application
2 | package testutils
3 |
4 | import (
5 | "os"
6 | "path/filepath"
7 | "testing"
8 | "time"
9 |
10 | "github.com/glebarez/sqlite"
11 | "github.com/starfleetcptn/gomft/internal/auth"
12 | "github.com/starfleetcptn/gomft/internal/config"
13 | "github.com/starfleetcptn/gomft/internal/db"
14 | "github.com/starfleetcptn/gomft/internal/email"
15 | "github.com/starfleetcptn/gomft/internal/scheduler"
16 | "golang.org/x/crypto/bcrypt"
17 | "gorm.io/gorm"
18 | )
19 |
20 | // SetupTestDB creates an in-memory SQLite database for testing
21 | func SetupTestDB(t *testing.T) *db.DB {
22 | gormDB, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
23 | if err != nil {
24 | t.Fatalf("Failed to open in-memory database: %v", err)
25 | }
26 |
27 | // Drop all tables to ensure a clean database
28 | err = gormDB.Migrator().DropTable(
29 | &db.User{},
30 | &db.PasswordHistory{},
31 | &db.PasswordResetToken{},
32 | &db.TransferConfig{},
33 | &db.Job{},
34 | &db.JobHistory{},
35 | &db.FileMetadata{},
36 | )
37 | if err != nil {
38 | t.Logf("Warning: Failed to drop tables: %v", err)
39 | }
40 |
41 | // Initialize the database schema
42 | err = gormDB.AutoMigrate(
43 | &db.User{},
44 | &db.PasswordHistory{},
45 | &db.PasswordResetToken{},
46 | &db.TransferConfig{},
47 | &db.Job{},
48 | &db.JobHistory{},
49 | &db.FileMetadata{},
50 | )
51 | if err != nil {
52 | t.Fatalf("Failed to migrate database: %v", err)
53 | }
54 |
55 | return &db.DB{DB: gormDB}
56 | }
57 |
58 | // CreateTestUser creates a test user in the database
59 | func CreateTestUser(t *testing.T, database *db.DB, email string, isAdmin bool) *db.User {
60 | // Generate hashed password using bcrypt directly
61 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte("testpassword"), bcrypt.DefaultCost)
62 | if err != nil {
63 | t.Fatalf("Failed to hash password: %v", err)
64 | }
65 |
66 | user := &db.User{
67 | Email: email,
68 | PasswordHash: string(hashedPassword),
69 | LastPasswordChange: time.Now(),
70 | }
71 | user.SetIsAdmin(isAdmin)
72 |
73 | if err := database.CreateUser(user); err != nil {
74 | t.Fatalf("Failed to create test user: %v", err)
75 | }
76 |
77 | return user
78 | }
79 |
80 | // SetupTestConfig creates a test configuration
81 | func SetupTestConfig(t *testing.T) *config.Config {
82 | tempDir, err := os.MkdirTemp("", "gomft-test-*")
83 | if err != nil {
84 | t.Fatalf("Failed to create temp directory: %v", err)
85 | }
86 | t.Cleanup(func() {
87 | os.RemoveAll(tempDir)
88 | })
89 |
90 | return &config.Config{
91 | ServerAddress: ":9090",
92 | DataDir: filepath.Join(tempDir, "data"),
93 | BackupDir: filepath.Join(tempDir, "backups"),
94 | JWTSecret: "test-jwt-secret",
95 | BaseURL: "http://test.example.com",
96 | Email: config.EmailConfig{
97 | Enabled: false,
98 | Host: "smtp.test.com",
99 | Port: 587,
100 | Username: "test@example.com",
101 | Password: "test-password",
102 | FromEmail: "test@example.com",
103 | FromName: "Test",
104 | EnableTLS: true,
105 | RequireAuth: true,
106 | },
107 | }
108 | }
109 |
110 | // SetupTestScheduler creates a mock scheduler for testing
111 | func SetupTestScheduler(t *testing.T) *scheduler.Scheduler {
112 | // In a real test, we would create a proper mock scheduler
113 | // For now, we return an empty scheduler
114 | return &scheduler.Scheduler{}
115 | }
116 |
117 | // SetupTestEmailService creates a mock email service for testing
118 | func SetupTestEmailService(t *testing.T) *email.Service {
119 | // In a real test, we would create a proper mock email service
120 | // For now, we return an empty email service
121 | return &email.Service{}
122 | }
123 |
124 | // GenerateTestToken generates a JWT token for testing
125 | func GenerateTestToken(userID uint, isAdmin bool, jwtSecret string) (string, error) {
126 | // In a real application, we would include email, but for testing purposes we can create a fake email
127 | email := "test@example.com"
128 | if isAdmin {
129 | email = "admin@example.com"
130 | }
131 |
132 | // Create token with 1 hour expiry
133 | expirationTime := 1 * time.Hour
134 | return auth.GenerateToken(userID, email, jwtSecret, expirationTime)
135 | }
136 |
--------------------------------------------------------------------------------
/internal/web/handlers.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "path/filepath"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/starfleetcptn/gomft/internal/config"
8 | "github.com/starfleetcptn/gomft/internal/db"
9 | "github.com/starfleetcptn/gomft/internal/email"
10 | "github.com/starfleetcptn/gomft/internal/scheduler"
11 | "github.com/starfleetcptn/gomft/internal/web/handlers"
12 | )
13 |
14 | // Handler is a wrapper around the handlers package
15 | type Handler struct {
16 | handlers *handlers.Handlers
17 | }
18 |
19 | // Global handlers instance for access from other packages
20 | var globalHandlersInstance *handlers.Handlers
21 |
22 | // GetHandlersInstance returns the global handlers instance and a boolean indicating if it's initialized
23 | func GetHandlersInstance() (*handlers.Handlers, bool) {
24 | return globalHandlersInstance, globalHandlersInstance != nil
25 | }
26 |
27 | // NewHandler creates a new Handler instance that delegates to the handlers package
28 | func NewHandler(database *db.DB, scheduler *scheduler.Scheduler, jwtSecret string, dbPath string, backupDir string, cfg *config.Config) (*Handler, error) {
29 | // Create email service instance
30 | emailService := email.NewService(cfg)
31 |
32 | // Use logs directory from config
33 | logsDir := filepath.Join(cfg.DataDir, "logs")
34 |
35 | // Create handlers instance
36 | handlersInstance := handlers.NewHandlers(database, scheduler, jwtSecret, dbPath, backupDir, logsDir, emailService)
37 |
38 | // Store the handlers instance globally
39 | globalHandlersInstance = handlersInstance
40 |
41 | return &Handler{
42 | handlers: handlersInstance,
43 | }, nil
44 | }
45 |
46 | // InitializeRoutes delegates route registration to the handlers package
47 | func (h *Handler) InitializeRoutes(router *gin.Engine) {
48 | // Register all routes through the handlers package
49 | h.handlers.RegisterRoutes(router)
50 | }
51 |
--------------------------------------------------------------------------------
/internal/web/handlers/basic_handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/starfleetcptn/gomft/components"
8 | "github.com/starfleetcptn/gomft/internal/auth"
9 | )
10 |
11 | // HandleHome handles the GET / route
12 | func (h *Handlers) HandleHome(c *gin.Context) {
13 | // Check for JWT token in cookie
14 | tokenCookie, err := c.Cookie("jwt_token")
15 | if err == nil && tokenCookie != "" {
16 | // Token exists, validate it
17 | claims, err := auth.ValidateToken(tokenCookie, h.JWTSecret)
18 | if err == nil && claims != nil {
19 | // Valid token, redirect to dashboard
20 | c.Redirect(http.StatusFound, "/dashboard")
21 | return
22 | }
23 | }
24 |
25 | // User is not logged in, show home page
26 | components.Home(c.Request.Context()).Render(c, c.Writer)
27 | }
--------------------------------------------------------------------------------
/internal/web/handlers/error_handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/starfleetcptn/gomft/components"
8 | )
9 |
10 | // HandleNotFound handles 404 Not Found errors
11 | func (h *Handlers) HandleNotFound(c *gin.Context, title string, message string) {
12 | if title == "" {
13 | title = "Page Not Found"
14 | }
15 | if message == "" {
16 | message = "The page you are looking for does not exist."
17 | }
18 | h.HandleError(c, 404, title, message, nil)
19 | }
20 |
21 | // HandleBadRequest handles 400 Bad Request errors
22 | func (h *Handlers) HandleBadRequest(c *gin.Context, title, message string) {
23 | h.HandleError(c, 400, title, message, nil)
24 | }
25 |
26 | // HandleUnauthorized handles 401 Unauthorized errors
27 | func (h *Handlers) HandleUnauthorized(c *gin.Context) {
28 | ctx := components.CreateTemplateContext(c)
29 | _ = components.UnauthorizedError(ctx).Render(ctx, c.Writer)
30 | }
31 |
32 | // HandleServerError handles 500 Internal Server errors
33 | func (h *Handlers) HandleServerError(c *gin.Context, err error) {
34 | var details string
35 | if err != nil {
36 | details = err.Error()
37 | }
38 | ctx := components.CreateTemplateContext(c)
39 | _ = components.ServerError(ctx, details).Render(ctx, c.Writer)
40 | }
41 |
42 | // HandleError handles generic errors with custom title and message
43 | func (h *Handlers) HandleError(c *gin.Context, code int, title, message string, err error) {
44 | var details string
45 | if err != nil {
46 | details = err.Error()
47 | }
48 | ctx := components.CreateTemplateContext(c)
49 | _ = components.ErrorPage(ctx, code, title, message, details).Render(ctx, c.Writer)
50 | }
51 |
52 | // RegisterErrorHandlers sets up custom error handlers for the router
53 | func (h *Handlers) RegisterErrorHandlers(router *gin.Engine) {
54 | // Register 404 handler
55 | router.NoRoute(func(c *gin.Context) {
56 | h.HandleNotFound(c, "", "")
57 | })
58 |
59 | // Register 405 handler (Method Not Allowed)
60 | router.NoMethod(func(c *gin.Context) {
61 | h.HandleError(c, 405, "Method Not Allowed",
62 | fmt.Sprintf("The %s method is not supported for this resource.", c.Request.Method), nil)
63 | })
64 |
65 | // Register function to recover from panics
66 | router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
67 | var err error
68 | if rec, ok := recovered.(error); ok {
69 | err = rec
70 | } else {
71 | err = fmt.Errorf("%v", recovered)
72 | }
73 | h.HandleServerError(c, err)
74 | c.Abort()
75 | }))
76 | }
77 |
--------------------------------------------------------------------------------
/internal/web/handlers/path_handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | // HandleCheckPath validates if a given path exists and is accessible
12 | func (h *Handlers) HandleCheckPath(c *gin.Context) {
13 | path := c.Query("path")
14 | if path == "" {
15 | c.JSON(http.StatusBadRequest, gin.H{
16 | "valid": false,
17 | "error": "No path provided",
18 | })
19 | return
20 | }
21 |
22 | // Clean and resolve the path
23 | path = filepath.Clean(path)
24 | absPath, err := filepath.Abs(path)
25 | if err != nil {
26 | c.JSON(http.StatusOK, gin.H{
27 | "valid": false,
28 | "error": "Invalid path format",
29 | })
30 | return
31 | }
32 |
33 | // Check if path exists
34 | info, err := os.Stat(absPath)
35 | if err != nil {
36 | if os.IsNotExist(err) {
37 | c.JSON(http.StatusOK, gin.H{
38 | "valid": false,
39 | "error": "Path does not exist",
40 | })
41 | return
42 | }
43 | c.JSON(http.StatusOK, gin.H{
44 | "valid": false,
45 | "error": "Error accessing path: " + err.Error(),
46 | })
47 | return
48 | }
49 |
50 | // Check if it's a directory
51 | if !info.IsDir() {
52 | c.JSON(http.StatusOK, gin.H{
53 | "valid": false,
54 | "error": "Path exists but is not a directory",
55 | })
56 | return
57 | }
58 |
59 | // Check if we have read access
60 | testFile := filepath.Join(absPath, ".gomft_test")
61 | f, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY, 0666)
62 | if err != nil {
63 | c.JSON(http.StatusOK, gin.H{
64 | "valid": false,
65 | "error": "Directory exists but is not writable",
66 | })
67 | return
68 | }
69 | f.Close()
70 | os.Remove(testFile)
71 |
72 | c.JSON(http.StatusOK, gin.H{
73 | "valid": true,
74 | "error": "",
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/internal/web/handlers/profile_handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/starfleetcptn/gomft/components"
8 | "github.com/starfleetcptn/gomft/internal/db"
9 | )
10 |
11 | // HandleProfile handles the GET /profile route
12 | func (h *Handlers) HandleProfile(c *gin.Context) {
13 | userID := c.GetUint("userID")
14 | var user db.User
15 | if err := h.DB.First(&user, userID).Error; err != nil {
16 | c.String(http.StatusInternalServerError, "Failed to retrieve user profile")
17 | return
18 | }
19 | components.Profile(c.Request.Context(), user).Render(c, c.Writer)
20 | }
21 |
22 | // HandleUpdateTheme handles the POST /profile/theme route
23 | func (h *Handlers) HandleUpdateTheme(c *gin.Context) {
24 | userID := c.GetUint("userID")
25 | theme := c.PostForm("theme")
26 |
27 | // Validate theme value
28 | validThemes := map[string]bool{
29 | "light": true,
30 | "dark": true,
31 | "system": true,
32 | }
33 |
34 | if !validThemes[theme] {
35 | c.Status(http.StatusBadRequest)
36 | return
37 | }
38 |
39 | // Update user theme preference
40 | var user db.User
41 | if err := h.DB.First(&user, userID).Error; err != nil {
42 | c.Status(http.StatusInternalServerError)
43 | return
44 | }
45 |
46 | user.Theme = theme
47 | if err := h.DB.Save(&user).Error; err != nil {
48 | c.Status(http.StatusInternalServerError)
49 | return
50 | }
51 |
52 | // Set theme cookie for client-side theme switching
53 | c.SetCookie("theme", theme, 60*60*24*365, "/", "", false, false)
54 |
55 | c.Status(http.StatusOK)
56 | }
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gomft",
3 | "version": "1.0.0",
4 | "description": "GoMFT static assets bundling",
5 | "type": "module",
6 | "scripts": {
7 | "build": "node build.js",
8 | "watch": "node build.js --watch",
9 | "postinstall": "npm run build"
10 | },
11 | "dependencies": {
12 | "@fortawesome/fontawesome-free": "^6.4.0",
13 | "alpinejs": "^3.13.5",
14 | "esbuild": "^0.20.1",
15 | "flowbite": "^2.2.1",
16 | "htmx.org": "^1.9.10",
17 | "tailwindcss": "^3.4.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/screenshots/audit.logs.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/audit.logs.gomft.png
--------------------------------------------------------------------------------
/screenshots/authentication.providers.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/authentication.providers.gomft.png
--------------------------------------------------------------------------------
/screenshots/dashboard.dark.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/dashboard.dark.gomft.png
--------------------------------------------------------------------------------
/screenshots/dashboard.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/dashboard.gomft.png
--------------------------------------------------------------------------------
/screenshots/database.tools.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/database.tools.gomft.png
--------------------------------------------------------------------------------
/screenshots/file.details.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/file.details.gomft.png
--------------------------------------------------------------------------------
/screenshots/file.metadata.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/file.metadata.gomft.png
--------------------------------------------------------------------------------
/screenshots/job.run.details.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/job.run.details.gomft.png
--------------------------------------------------------------------------------
/screenshots/notification.service.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/notification.service.gomft.png
--------------------------------------------------------------------------------
/screenshots/notifications.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/notifications.gomft.png
--------------------------------------------------------------------------------
/screenshots/role.management.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/role.management.gomft.png
--------------------------------------------------------------------------------
/screenshots/scheduled.jobs.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/scheduled.jobs.gomft.png
--------------------------------------------------------------------------------
/screenshots/transfer.config.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/transfer.config.gomft.png
--------------------------------------------------------------------------------
/screenshots/transfer.history.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/transfer.history.gomft.png
--------------------------------------------------------------------------------
/screenshots/user.management.gomft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/screenshots/user.management.gomft.png
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/favicon.ico
--------------------------------------------------------------------------------
/static/img/authentik.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
23 |
24 |
--------------------------------------------------------------------------------
/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StarFleetCPTN/GoMFT/88d0ac815a5c9ab89cd41bd1b85721ac7bc4c55a/static/img/logo.png
--------------------------------------------------------------------------------
/static/img/oidc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/static/img/pocket-id.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/static/img/saml.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/static/js/init.js:
--------------------------------------------------------------------------------
1 | // Add script to ensure dark mode is properly applied
2 | document.addEventListener('DOMContentLoaded', function() {
3 | // Apply body dark class when theme changes
4 | const isDark = document.documentElement.classList.contains('dark');
5 | if (isDark) {
6 | document.body.classList.add('dark');
7 |
8 | // Also apply to containers
9 | const jobsContainer = document.getElementById('jobs-container');
10 | const configsContainer = document.getElementById('configs-container');
11 |
12 | if (jobsContainer) jobsContainer.classList.add('dark');
13 | if (configsContainer) configsContainer.classList.add('dark');
14 | }
15 |
16 | // Initialize admin dropdown toggle if available
17 | const adminDropdownToggle = document.querySelector('[data-collapse-toggle="dropdown-settings"]');
18 | const adminDropdown = document.getElementById('dropdown-settings');
19 |
20 | if (adminDropdownToggle && adminDropdown) {
21 | // Check if we should show the dropdown (if current page is under admin section)
22 | const currentPath = window.location.pathname;
23 | if (currentPath.startsWith('/admin')) {
24 | adminDropdown.classList.remove('hidden');
25 | }
26 |
27 | // Add click event listener
28 | adminDropdownToggle.addEventListener('click', function() {
29 | adminDropdown.classList.toggle('hidden');
30 | });
31 | }
32 | });
--------------------------------------------------------------------------------
/static/js/vendor.js:
--------------------------------------------------------------------------------
1 | // Import HTMX
2 | import 'htmx.org';
3 |
4 | // Import Alpine.js
5 | import Alpine from 'alpinejs';
6 | window.Alpine = Alpine;
7 |
8 | // Import Flowbite
9 | import 'flowbite';
10 | import 'flowbite/dist/flowbite.css';
11 |
12 | // Initialize Flowbite components
13 | document.addEventListener('DOMContentLoaded', () => {
14 | // Initialize Alpine.js
15 | Alpine.start();
16 |
17 | // Initialize Flowbite modals
18 | const modalTriggers = document.querySelectorAll('[data-modal-target]');
19 | modalTriggers.forEach(trigger => {
20 | const targetId = trigger.getAttribute('data-modal-target');
21 | const targetModal = document.getElementById(targetId);
22 | if (targetModal) {
23 | // Show modal
24 | trigger.addEventListener('click', () => {
25 | targetModal.classList.remove('hidden');
26 | targetModal.classList.add('flex');
27 | // Add backdrop
28 | document.body.style.overflow = 'hidden';
29 | });
30 |
31 | // Handle modal hide buttons
32 | const hideButtons = targetModal.querySelectorAll('[data-modal-hide]');
33 | hideButtons.forEach(button => {
34 | button.addEventListener('click', () => {
35 | targetModal.classList.add('hidden');
36 | targetModal.classList.remove('flex');
37 | // Remove backdrop
38 | document.body.style.overflow = '';
39 | });
40 | });
41 |
42 | // Handle clicking outside the modal
43 | targetModal.addEventListener('click', (event) => {
44 | if (event.target === targetModal || event.target.classList.contains('fixed')) {
45 | targetModal.classList.add('hidden');
46 | targetModal.classList.remove('flex');
47 | // Remove backdrop
48 | document.body.style.overflow = '';
49 | }
50 | });
51 | }
52 | });
53 | });
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./components/**/*.{html,js,templ,go}",
5 | "./node_modules/flowbite/**/*.js"
6 | ],
7 | darkMode: 'class',
8 | theme: {
9 | extend: {
10 | colors: {
11 | primary: {
12 | 50: '#f0f9ff',
13 | 100: '#e0f2fe',
14 | 200: '#bae6fd',
15 | 300: '#7dd3fc',
16 | 400: '#38bdf8',
17 | 500: '#0ea5e9',
18 | 600: '#0284c7',
19 | 700: '#0369a1',
20 | 800: '#075985',
21 | 900: '#0c4a6e',
22 | 950: '#082f49',
23 | }
24 | },
25 | fontFamily: {
26 | sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
27 | }
28 | }
29 | },
30 | plugins: [
31 | require('flowbite/plugin')
32 | ],
33 | }
--------------------------------------------------------------------------------