diff --git a/backend/internal/hostupdate/manager_ops.go b/backend/internal/hostupdate/manager_ops.go index b056c3be3..61cfe8426 100644 --- a/backend/internal/hostupdate/manager_ops.go +++ b/backend/internal/hostupdate/manager_ops.go @@ -123,6 +123,14 @@ func (m *Manager) preflight(composePath, targetVersion string) error { if _, err := os.Stat(m.envPath()); err != nil { return fmt.Errorf("读取部署环境:%w", err) } + if m.config.SelfUpdate { + if strings.TrimSpace(m.config.BinaryPath) == "" { + return errors.New("检查 Host Updater 安装目录:二进制路径为空") + } + if err := checkWritableDirectory(filepath.Dir(m.config.BinaryPath)); err != nil { + return fmt.Errorf("检查 Host Updater 安装目录:%w", err) + } + } var disk syscall.Statfs_t if err := syscall.Statfs(m.config.BackupDir, &disk); err != nil { return fmt.Errorf("检查备份磁盘空间:%w", err) @@ -144,6 +152,26 @@ func (m *Manager) preflight(composePath, targetVersion string) error { return nil } +func checkWritableDirectory(directory string) error { + directory = strings.TrimSpace(directory) + if directory == "" { + return errors.New("Host Updater 二进制路径为空") + } + temporary, err := os.CreateTemp(directory, ".updater-write-test-*") + if err != nil { + return err + } + name := temporary.Name() + if closeErr := temporary.Close(); closeErr != nil { + _ = os.Remove(name) + return closeErr + } + if removeErr := os.Remove(name); removeErr != nil { + return removeErr + } + return nil +} + func (m *Manager) prepareUpdaterBinary(targetVersion string) (string, error) { if !m.config.SelfUpdate { return "", nil diff --git a/backend/internal/hostupdate/manager_ops_test.go b/backend/internal/hostupdate/manager_ops_test.go index dea114057..c9add7a16 100644 --- a/backend/internal/hostupdate/manager_ops_test.go +++ b/backend/internal/hostupdate/manager_ops_test.go @@ -129,3 +129,20 @@ func TestCreateBackupReadsBackendDataAsRoot(t *testing.T) { } t.Fatalf("backend data backup did not use root: %#v", runner.calls) } + +func TestCheckWritableDirectory(t *testing.T) { + directory := t.TempDir() + if err := checkWritableDirectory(directory); err != nil { + t.Fatalf("writable directory rejected: %v", err) + } + entries, err := os.ReadDir(directory) + if err != nil { + t.Fatal(err) + } + if len(entries) != 0 { + t.Fatalf("write probe was not cleaned up: %v", entries) + } + if err := checkWritableDirectory(filepath.Join(directory, "missing")); err == nil { + t.Fatal("missing directory was accepted") + } +} diff --git a/scripts/install-host-updater.sh b/scripts/install-host-updater.sh index 04dec91d0..ca72f6010 100755 --- a/scripts/install-host-updater.sh +++ b/scripts/install-host-updater.sh @@ -97,7 +97,7 @@ install_service() { 'PrivateTmp=true' \ 'ProtectHome=true' \ 'ProtectSystem=full' \ - "ReadWritePaths=${INSTALL_DIR} /var/lib/open-ai-canvas-updater ${SOCKET_DIR} ${UPDATER_BIN}" \ + "ReadWritePaths=${INSTALL_DIR} /var/lib/open-ai-canvas-updater ${SOCKET_DIR} /usr/local/bin" \ '' \ '[Install]' \ 'WantedBy=multi-user.target' > "$temporary_service"