}
@@ -176,14 +220,14 @@ templ formFields(data StorageProviderFormData) {
@@ -207,31 +251,31 @@ templ formFields(data StorageProviderFormData) {
-
+
if data.IsEdit {
Leave empty to keep the current secret key
}
@@ -245,13 +289,13 @@ templ formFields(data StorageProviderFormData) {
-
+
-
+
if data.IsEdit {
Leave empty to keep the current client secret
}
@@ -260,7 +304,7 @@ templ formFields(data StorageProviderFormData) {
@@ -291,7 +335,7 @@ templ formFields(data StorageProviderFormData) {
@@ -318,62 +362,107 @@ templ providerFormScript() {
// Set current active fields on page load
document.addEventListener('DOMContentLoaded', function() {
toggleProviderFields();
+
+ // Add event listener to track if user manually changes the port
+ const portField = document.getElementById('port');
+ portField.addEventListener('input', function() {
+ // Mark the field as user-modified
+ this.dataset.userModified = 'true';
+ });
});
function toggleProviderFields() {
+ const provider = document.getElementById('type').value;
+
// Hide all provider fields first
const providerFieldsets = document.querySelectorAll('.provider-fields');
providerFieldsets.forEach(fieldset => {
fieldset.classList.add('hidden');
});
- // Show the appropriate fields based on the selected provider type
- const providerType = document.getElementById('type').value;
- console.log("Provider type selected:", providerType);
+ // Auto-populate port based on provider type
+ const portField = document.getElementById('port');
+ const isPortEmpty = portField.value === '';
+ const userModified = portField.dataset.userModified === 'true';
- // SFTP/FTP/SMB fields
- if (['sftp', 'ftp', 'smb'].includes(providerType)) {
+ // Only set default port if field is empty or hasn't been modified by user
+ if (isPortEmpty || !userModified) {
+ if (provider === 'sftp') {
+ portField.value = '22';
+ } else if (provider === 'hetzner') {
+ portField.value = '23';
+ } else if (provider === 'ftp') {
+ portField.value = '21';
+ } else if (provider === 'smb') {
+ portField.value = '445';
+ } else if (provider === 'webdav' || provider === 'nextcloud') {
+ portField.value = '443';
+ } else {
+ portField.value = '';
+ }
+ // Reset user modified flag if we're setting it programmatically
+ portField.dataset.userModified = 'false';
+ }
+
+ // Show the appropriate fields based on the selected provider type
+ console.log("Provider type selected:", provider);
+
+ // Server-based connection fields (SFTP, FTP, Hetzner)
+ if (['sftp', 'ftp', 'hetzner'].includes(provider)) {
document.getElementById('sftp-ftp-fields').classList.remove('hidden');
- // SFTP-specific fields
- if (providerType === 'sftp') {
+ // SFTP/Hetzner-specific fields
+ if (provider === 'sftp' || provider === 'hetzner') {
document.getElementById('key-file-field').classList.remove('hidden');
}
- // SMB-specific fields
- if (providerType === 'smb') {
- document.getElementById('domain-field').classList.remove('hidden');
- }
-
// FTP-specific fields
- if (providerType === 'ftp') {
+ if (provider === 'ftp') {
document.getElementById('passive-mode-field').classList.remove('hidden');
}
}
- // S3 fields
- if (providerType === 's3' || providerType === 'hetzner') {
+ // WebDAV-based fields
+ if (['webdav', 'nextcloud'].includes(provider)) {
+ document.getElementById('sftp-ftp-fields').classList.remove('hidden');
+ // No special fields currently for WebDAV
+ }
+
+ // SMB-specific fields
+ if (provider === 'smb') {
+ document.getElementById('sftp-ftp-fields').classList.remove('hidden');
+ document.getElementById('domain-field').classList.remove('hidden');
+ }
+
+ // S3-compatible storage
+ if (['s3', 'wasabi', 'minio', 'b2'].includes(provider)) {
document.getElementById('s3-fields').classList.remove('hidden');
}
- // Cloud storage fields
- if (['onedrive', 'gdrive', 'gphotos'].includes(providerType)) {
+ // Google services
+ if (['gdrive', 'gphotos'].includes(provider)) {
document.getElementById('cloud-fields').classList.remove('hidden');
// Google Drive specific fields
- if (providerType === 'gdrive') {
+ if (provider === 'gdrive') {
document.getElementById('drive-id-field').classList.remove('hidden');
document.getElementById('team-drive-field').classList.remove('hidden');
}
// Google Photos specific fields
- if (providerType === 'gphotos') {
+ if (provider === 'gphotos') {
document.getElementById('readonly-field').classList.remove('hidden');
}
}
+ // OneDrive
+ if (provider === 'onedrive') {
+ document.getElementById('cloud-fields').classList.remove('hidden');
+ // No special fields for OneDrive currently
+ }
+
// Local filesystem fields
- if (providerType === 'local') {
+ if (provider === 'local') {
document.getElementById('local-fields').classList.remove('hidden');
}
}
diff --git a/internal/db/storage_provider.go b/internal/db/storage_provider.go
index 943feca..0419748 100644
--- a/internal/db/storage_provider.go
+++ b/internal/db/storage_provider.go
@@ -18,6 +18,11 @@ const (
ProviderTypeSMB StorageProviderType = "smb"
ProviderTypeHetzner StorageProviderType = "hetzner"
ProviderTypeLocal StorageProviderType = "local"
+ ProviderTypeWebDAV StorageProviderType = "webdav"
+ ProviderTypeNextcloud StorageProviderType = "nextcloud"
+ ProviderTypeB2 StorageProviderType = "b2"
+ ProviderTypeWasabi StorageProviderType = "wasabi"
+ ProviderTypeMinio StorageProviderType = "minio"
)
// StorageProvider represents a connection to a storage service
@@ -164,7 +169,9 @@ func (sp *StorageProvider) SetAuthenticated(value bool) {
func (sp *StorageProvider) IsOAuthProvider() bool {
return sp.Type == ProviderTypeOneDrive ||
sp.Type == ProviderTypeGoogleDrive ||
- sp.Type == ProviderTypeGooglePhoto
+ sp.Type == ProviderTypeGooglePhoto ||
+ sp.Type == ProviderTypeWebDAV ||
+ sp.Type == ProviderTypeNextcloud
}
// RequiresEncryption returns true if the provider has sensitive fields that need encryption
@@ -183,7 +190,7 @@ func (sp *StorageProvider) GetSensitiveFields() map[string]string {
if sp.Password != "" {
sensitiveFields["Password"] = sp.Password
}
- case ProviderTypeS3:
+ case ProviderTypeS3, ProviderTypeWasabi, ProviderTypeMinio, ProviderTypeB2:
if sp.SecretKey != "" {
sensitiveFields["SecretKey"] = sp.SecretKey
}
diff --git a/package-lock.json b/package-lock.json
index 373ebaf..7faf9c3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "gomft",
"version": "1.0.0",
+ "hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"alpinejs": "^3.13.5",
@@ -14,6 +15,11 @@
"flowbite": "^2.2.1",
"htmx.org": "^1.9.10",
"tailwindcss": "^3.4.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.46.0",
+ "dotenv": "^16.5.0",
+ "expect-playwright": "^0.8.0"
}
},
"node_modules/@alloc/quick-lru": {
@@ -516,6 +522,22 @@
"node": ">=14"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
+ "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.51.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -748,6 +770,19 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -798,6 +833,13 @@
"@esbuild/win32-x64": "0.20.2"
}
},
+ "node_modules/expect-playwright": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz",
+ "integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -1247,6 +1289,53 @@
"node": ">= 6"
}
},
+ "node_modules/playwright": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
+ "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.51.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
+ "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
diff --git a/package.json b/package.json
index d07a96f..8415176 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,11 @@
"scripts": {
"build": "node build.js",
"watch": "node build.js --watch",
- "postinstall": "npm run build"
+ "postinstall": "npm run build",
+ "test": "playwright test",
+ "test:ui": "playwright test --ui",
+ "test:headed": "playwright test --headed",
+ "test:debug": "playwright test --debug"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
@@ -15,5 +19,10 @@
"flowbite": "^2.2.1",
"htmx.org": "^1.9.10",
"tailwindcss": "^3.4.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.46.0",
+ "dotenv": "^16.5.0",
+ "expect-playwright": "^0.8.0"
}
}
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 0000000..37adfd4
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,64 @@
+// @ts-check
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 30 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://localhost:8080',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+
+ /* Capture screenshot on failure */
+ screenshot: 'only-on-failure',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'go run main.go',
+ url: 'http://localhost:8080',
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+});
\ No newline at end of file