Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
989 changes: 294 additions & 695 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions api/.air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
cmd = "go build -o ./tmp/main.exe ."
bin = "tmp/main.exe"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "migrations", "templates"]
exclude_file = ["api/versions.json", "api/tmp/*", "api/logs/*", "doc/openapi.json"]
Expand Down
2 changes: 1 addition & 1 deletion api/api/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"version": "v1",
"status": "active",
"release_date": "2025-10-22T16:45:33.662757+05:30",
"release_date": "2025-10-26T11:19:06.7537736+05:30",
"end_of_life": "0001-01-01T00:00:00Z",
"changes": [
"Initial API version"
Expand Down
12 changes: 12 additions & 0 deletions api/internal/features/deploy/docker/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type DockerRepository interface {
ComposeUp(composeFilePath string, envVars map[string]string) error
ComposeDown(composeFilePath string) error
ComposeBuild(composeFilePath string, envVars map[string]string) error
ComposePull(composeFilePath string) error
RemoveImage(imageName string, opts image.RemoveOptions) error
PruneBuildCache(opts types.BuildCachePruneOptions) error
PruneImages(opts filters.Args) (image.PruneReport, error)
Expand Down Expand Up @@ -365,6 +366,17 @@ func (s *DockerService) ComposeBuild(composeFilePath string, envVars map[string]
return nil
}

// ComposePull pulls the Docker Compose services
func (s *DockerService) ComposePull(composeFilePath string) error {
client := ssh.NewSSH()
command := fmt.Sprintf("docker compose -f %s pull", composeFilePath)
output, err := client.RunCommand(command)
if err != nil {
return fmt.Errorf("failed to pull docker compose services: %v, output: %s", err, output)
}
return nil
}

// RemoveImage removes an image from the Docker host.
//
// This function removes an image from the Docker host using the Docker client.
Expand Down
8 changes: 8 additions & 0 deletions api/internal/features/extension/engine/docker_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (dockerComposeModule) Execute(_ *ssh.SSH, step types.SpecStep, vars map[str
"up": func() (string, func(), error) { return composeUp(svc, file) },
"down": func() (string, func(), error) { return composeDown(svc, file) },
"build": func() (string, func(), error) { return composeBuild(svc, file) },
"pull": func() (string, func(), error) { return composePull(svc, file) },
}

h, ok := handlers[action]
Expand Down Expand Up @@ -72,6 +73,13 @@ func composeBuild(svc *deploydocker.DockerService, file string) (string, func(),
return "compose build", nil, nil
}

func composePull(svc *deploydocker.DockerService, file string) (string, func(), error) {
if err := svc.ComposePull(file); err != nil {
return "", nil, err
}
return "compose pull", nil, nil
}

func init() {
RegisterModule(dockerComposeModule{})
}
2 changes: 1 addition & 1 deletion api/internal/features/extension/parser/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (p *Parser) validateDockerComposeStep(step ExecutionStep) error {
}
action, _ := step.Properties["action"].(string)
switch action {
case "up", "down", "build":
case "up", "down", "build", "pull":
return nil
default:
return fmt.Errorf("unsupported docker_compose action: %s", action)
Expand Down
14 changes: 7 additions & 7 deletions api/internal/features/file-manager/service/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
"os"
"path/filepath"
"syscall"
// "syscall"
"time"

"github.com/pkg/sftp"
Expand Down Expand Up @@ -91,15 +91,15 @@ func (f *FileManagerService) ListFiles(path string) ([]FileData, error) {
}
}

sysInfo := info.Sys()
// sysInfo := info.Sys()
var ownerId, groupId int64
var permissions int64

if statInfo, ok := sysInfo.(*syscall.Stat_t); ok {
ownerId = int64(statInfo.Uid)
groupId = int64(statInfo.Gid)
permissions = int64(statInfo.Mode & 0777)
}
// if statInfo, ok := sysInfo.(*syscall.Stat_t); ok {
// ownerId = int64(statInfo.Uid)
// groupId = int64(statInfo.Gid)
// permissions = int64(statInfo.Mode & 0777)
// }
Comment on lines +94 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Breaking change: File permission and ownership data is no longer returned.

Commenting out the stat extraction logic means the API will now return 0 for permissions, owner_id, and group_id fields instead of the actual system values. This is a breaking behavioral change that could impact:

  • Clients that rely on permission data for access control decisions
  • UI components displaying file ownership information
  • Security auditing or compliance features

If this change addresses platform compatibility issues (e.g., syscall.Stat_t is Unix-specific), consider using Go build tags instead:

//go:build unix

package service

import "syscall"

func extractStatInfo(info os.FileInfo) (ownerId, groupId, permissions int64) {
	if sysInfo := info.Sys(); sysInfo != nil {
		if statInfo, ok := sysInfo.(*syscall.Stat_t); ok {
			return int64(statInfo.Uid), int64(statInfo.Gid), int64(statInfo.Mode & 0777)
		}
	}
	return 0, 0, 0
}

Alternatively, if these fields are no longer supported, remove them from the FileData struct to avoid returning misleading zero values.

Run the following script to check for API consumers that may depend on these fields:


🏁 Script executed:

#!/bin/bash
# Search for usage of permissions, owner_id, or group_id fields from FileData responses

rg -nP --type=go -C3 '\b(Permissions|OwnerId|GroupId)\b' -g '!**/service/manager.go'

Length of output: 2905


</script>

🏁 Script executed:

#!/bin/bash
# Find FileData struct definition and API usage

echo "=== FileData struct definition ==="
rg -nP --type=go -B2 -A8 'type FileData struct' -g '!**/test/**'

echo -e "\n=== Handlers returning FileData ==="
rg -nP --type=go -B3 -A3 'FileData' -g '!**/service/manager.go' | head -50

Length of output: 1102


</script>

🏁 Script executed:

#!/bin/bash
# Get complete FileData struct definition and find API consumers

echo "=== Complete FileData struct ==="
sed -n '145,200p' api/internal/features/file-manager/service/manager.go

echo -e "\n=== API routes/handlers using file manager ==="
rg -nP --type=go 'FileData|file.*manager' api/internal/features/file-manager/handler/ api/internal/routes/ 2>/dev/null | head -40

echo -e "\n=== Check manager.go for OwnerId and GroupId fields ==="
rg -nP --type=go 'OwnerId|GroupId' api/internal/features/file-manager/

Length of output: 2040


</script>

🏁 Script executed:

#!/bin/bash
# View the full context of the commented code and assignments

echo "=== Context around lines 94-120 ==="
sed -n '80,125p' api/internal/features/file-manager/service/manager.go

echo -e "\n=== Search for routes/handlers in file-manager ==="
find api/internal/features/file-manager -name '*.go' -type f | xargs grep -l 'http\|route\|handler' | head -10

echo -e "\n=== List all files in file-manager ==="
find api/internal/features/file-manager -name '*.go' -type f

Length of output: 2121


</script>

🏁 Script executed:

#!/bin/bash
# Check test expectations and controller exposure

echo "=== Manager test file (checking for permission/owner assertions) ==="
cat api/internal/features/file-manager/tests/manager_test.go

echo -e "\n=== List files controller (checking HTTP exposure) ==="
cat api/internal/features/file-manager/controller/list_files.go | head -60

Length of output: 4469


Breaking change: File permission and ownership data is no longer returned.

Commenting out the stat extraction logic means the API will now return 0 for permissions, owner_id, and group_id fields instead of actual system values. This is confirmed as a breaking behavioral change—these fields are part of the FileData struct and are exposed via the HTTP API through the ListFiles controller.

Clients that rely on permission data for access control decisions, UI display of file ownership, or security auditing will now receive zero values.

If this change addresses platform compatibility issues (e.g., syscall.Stat_t being Unix-specific), use Go build tags instead:

//go:build unix

package service

import "syscall"

func extractStatInfo(info os.FileInfo) (ownerId, groupId, permissions int64) {
	if sysInfo := info.Sys(); sysInfo != nil {
		if statInfo, ok := sysInfo.(*syscall.Stat_t); ok {
			return int64(statInfo.Uid), int64(statInfo.Gid), int64(statInfo.Mode & 0777)
		}
	}
	return 0, 0, 0
}

Alternatively, if these fields are no longer supported on all platforms, remove them from the FileData struct to avoid returning misleading zero values.

🤖 Prompt for AI Agents
In api/internal/features/file-manager/service/manager.go around lines 94-102 the
stat extraction was commented out causing permissions, owner_id and group_id to
always be zero; restore platform-safe stat extraction by moving the
syscall-dependent logic into a dedicated helper implemented with Go build tags
(e.g., an extractStatInfo in a file with //go:build unix that uses
syscall.Stat_t and returns ownerId, groupId, permissions) and provide a non-unix
fallback implementation that returns zeros so the build remains portable;
alternatively, if ownership/permissions cannot be supported across all targets,
remove those fields from FileData and update API types/handlers accordingly.


fullPath := filepath.Join(path, info.Name())

Expand Down
2 changes: 1 addition & 1 deletion api/internal/features/supertokens/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Init(appInstance *storage.App) {
websiteBasePath := "/auth"

err := supertokens.Init(supertokens.TypeInput{
Debug: true,
Debug: false,
Supertokens: &supertokens.ConnectionInfo{
ConnectionURI: config.Supertokens.ConnectionURI,
APIKey: config.Supertokens.APIKey,
Expand Down
80 changes: 80 additions & 0 deletions api/templates/deploy-adgaurd-home.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
metadata:
id: "deploy-adguard-home"
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix filename typo: "adgaurd" should be "adguard".

Line 2 shows the id as "deploy-adguard-home" but the filename is deploy-adgaurd-home.yaml. The filename is misspelled (missing 'u' in "adguard"). For consistency and clarity, rename the file to deploy-adguard-home.yaml.

🤖 Prompt for AI Agents
In api/templates/deploy-adgaurd-home.yaml around lines 1 to 2, the filename
contains a typo ("adgaurd"); rename the file to deploy-adguard-home.yaml to
match the id and correct spelling, and update any references (scripts, CI,
manifests, imports) that point to the old filename to use the new
deploy-adguard-home.yaml name.

name: "AdGuard Home"
description: "AdGuard Home is a network-wide software for blocking ads and trackers via DNS filtering."
author: "Nixopus Team"
icon: "🛡️"
category: "Containers"
type: "install"
version: "1.0.0"
isVerified: false

variables:
image:
type: "string"
description: "Docker image for AdGuard Home"
default: "adguard/adguardhome"
is_required: true
tag:
type: "string"
description: "Docker image tag"
default: "latest"
is_required: true
container_name:
type: "string"
description: "Name of the AdGuard Home container"
default: "adguard-home"
is_required: true
host_port:
type: "integer"
description: "Host port to expose AdGuard Home web interface"
default: 3000
is_required: true
container_port:
type: "integer"
description: "Container port AdGuard Home listens on"
default: 3000
is_required: true
dns_port:
type: "integer"
description: "DNS port to expose"
default: 53
is_required: true
data_volume:
type: "string"
description: "Host path to mount for persistent data"
default: "./adguard_data"
is_required: true

execution:
run:
- name: "Pull AdGuard Home image"
type: "docker"
properties:
action: "pull"
image: "{{ image }}"
tag: "{{ tag }}"
timeout: 300

- name: "Run AdGuard Home container"
type: "docker"
properties:
action: "run"
name: "{{ container_name }}"
image: "{{ image }}"
tag: "{{ tag }}"
ports:
- "{{ host_port }}:{{ container_port }}"
- "{{ dns_port }}:{{ dns_port }}"
volumes:
- "{{ data_volume }}:/opt/adguardhome/work"
- "{{ data_volume }}:/opt/adguardhome/conf"
restart: "unless-stopped"
timeout: 180

# validate:
# - name: "Check HTTP response from AdGuard Home"
# type: "command"
# properties:
# cmd: "curl -fsS -o /dev/null -w '%{http_code}\\n' http://localhost:{{ host_port }}/ | grep -E '^(200|301|302)$'"
# timeout: 60
91 changes: 91 additions & 0 deletions api/templates/deploy-alist.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
metadata:
id: "deploy-alist"
name: "AList"
description: "AList is a file list/WebDAV program that supports multiple storage providers and provides a self-hosted file sharing/indexing solution."
author: "Nixopus Team"
icon: "📁"
category: "Containers"
type: "install"
version: "1.0.0"
isVerified: false

variables:
image:
type: "string"
description: "Docker image for AList"
default: "xhofe/alist"
is_required: true
tag:
type: "string"
description: "Docker image tag for AList"
default: "latest"
is_required: true
container_name:
type: "string"
description: "Name of the AList container"
default: "alist"
is_required: true
host_port:
type: "integer"
description: "Host port to expose for AList"
default: 5244
is_required: true
container_port:
type: "integer"
description: "Container port AList listens on"
default: 5244
is_required: true
data_volume:
type: "string"
description: "Host path to mount for AList persistent data"
default: "./alist_data"
is_required: true
PUID:
type: "integer"
description: "Host user ID to run container"
default: 0
is_required: false
PGID:
type: "integer"
description: "Host group ID to run container"
default: 0
is_required: false
UMASK:
type: "string"
description: "Umask for container file permissions"
default: "022"
is_required: false

execution:
run:
- name: "Pull AList image"
type: "docker"
properties:
action: "pull"
image: "{{ image }}"
tag: "{{ tag }}"
timeout: 300

- name: "Run AList container"
type: "docker"
properties:
action: "run"
name: "{{ container_name }}"
image: "{{ image }}"
tag: "{{ tag }}"
ports: "{{ host_port }}:{{ container_port }}"
volumes:
- "{{ data_volume }}:/opt/alist/data"
environment:
- "PUID={{ PUID }}"
- "PGID={{ PGID }}"
- "UMASK={{ UMASK }}"
restart: "unless-stopped"
timeout: 180

# validate:
# - name: "Check HTTP response from AList"
# type: "command"
# properties:
# cmd: "curl -fsS -o /dev/null -w '%{http_code}\\n' http://localhost:{{ host_port }} | grep -E '^(200|301|302)$'"
# timeout: 60
63 changes: 63 additions & 0 deletions api/templates/deploy-apache-superset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
metadata:
id: "deploy-apache-superset"
name: "Apache Superset"
description: "Apache Superset is a modern data exploration and visualization platform designed for data analysis and dashboarding."
author: "Nixopus Team"
icon: "📊"
category: "Containers"
type: "install"
version: "1.0.0"
isVerified: false

variables:
image:
type: "string"
description: "Docker image for Apache Superset"
default: "apache/superset"
is_required: true

tag:
type: "string"
description: "Docker image tag"
default: "latest"
is_required: true
Comment on lines +19 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid "latest" tag; use pinned versions for reproducibility.

Using :latest tags in production should be avoided to ensure predictable builds. The default "latest" tag makes deployments non-deterministic and can introduce breaking changes across deployments.

Recommend changing the default to a specific version (e.g., "3.0.2") to ensure consistent behavior across environments.

  tag:
    type: "string"
    description: "Docker image tag"
-   default: "latest"
+   default: "3.0.2"
    is_required: true

Alternatively, if supporting multiple versions is desired, provide clear documentation on the implications of using "latest" or require users to explicitly specify a version.

🤖 Prompt for AI Agents
In api/templates/deploy-apache-superset.yaml around lines 19 to 23, the template
default uses the unstable "latest" Docker tag which makes deployments
non-deterministic; change the default to a concrete, tested version (e.g.,
"3.0.2") or remove the default so users must specify a version, and update any
docs/comments to recommend pinning image tags for reproducible deployments.


container_name:
type: "string"
description: "Name of the container"
default: "superset"
is_required: true

host_port:
type: "integer"
description: "Host port to expose Superset"
default: 8088
is_required: true

container_port:
type: "integer"
description: "Container port for Superset service"
default: 8088
is_required: true

execution:
run:
- name: "Pull Apache Superset image"
type: "docker"
properties:
action: "pull"
image: "{{ image }}"
tag: "{{ tag }}"
timeout: 300

- name: "Run Apache Superset container"
type: "docker"
properties:
action: "run"
name: "{{ container_name }}"
image: "{{ image }}"
tag: "{{ tag }}"
ports: "{{ host_port }}:{{ container_port }}"
restart: "unless-stopped"
timeout: 180

Loading