Files
sanspie fac6fa35a4
ci-platforms / darwin arm64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 7m29s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 14m13s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: error
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
fix: stabilize CI and prepare docs release
Keep recovery copies private on Windows, make cross-platform test fixtures deterministic, and preserve the updated Art and documentation contracts.

Refresh workspace screenshot references for the intentional factory layouts and restore the earlier Russian palette reference after before/after comparison. Keep the existing screenshot tolerances.
2026-11-09 16:24:24 +03:00

124 lines
3.5 KiB
Go

package document
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"aego/core/ids"
)
// WriteRecoveryCopy exports dirty in-memory documents only after the user
// chooses an existing destination directory. It never replaces the project's
// recovery or adopts/restores one. Existing files are never overwritten.
// If any write fails the returned directory may contain a partial copy; the
// caller must report that it is incomplete, not claim recovery succeeded.
func (m *Manager) WriteRecoveryCopy(directory string) (string, int, error) {
if m == nil || !filepath.IsAbs(directory) {
return "", 0, fmt.Errorf("choose an existing absolute backup directory")
}
for _, d := range m.assets {
if d.Binding.Dirty != nil && d.Binding.Dirty() && d.Binding.Recovery == nil {
return "", 0, fmt.Errorf("%s cannot be exported as a recovery copy", d.Path)
}
}
root, err := os.OpenRoot(directory)
if err != nil {
return "", 0, err
}
defer root.Close()
name := "aego-recovery-" + ids.NewAssetID().String()
var output string
var copyDir *privateRecoveryDir
defer func() {
if copyDir != nil {
_ = copyDir.Close()
}
}()
count := 0
write := func(file string, data []byte) error {
if copyDir == nil {
var created bool
copyDir, created, err = createPrivateRecoveryDir(root, name)
if created {
output = filepath.Join(directory, name)
}
if err != nil {
return err
}
}
f, err := copyDir.CreateFile(file)
if err != nil {
return err
}
n, writeErr := f.Write(data)
if writeErr == nil && n != len(data) {
writeErr = io.ErrShortWrite
}
var syncErr error
if runtime.GOOS != "js" && runtime.GOOS != "wasip1" {
syncErr = f.Sync()
}
return errors.Join(writeErr, syncErr, f.Close())
}
for _, d := range m.docs {
if !d.Dirty() {
continue
}
if err := write(filepath.Base(m.recoveryFile(d.Path)), m.guarded(d, recoveryBytes)); err != nil {
return output, count, err
}
count++
}
for _, d := range m.assets {
if d.Binding.Dirty == nil || !d.Binding.Dirty() {
continue
}
data, err := d.Binding.Recovery()
if err != nil {
return output, count, err
}
if err := write(filepath.Base(m.assetRecoveryPath(d)), data); err != nil {
return output, count, err
}
count++
}
for _, d := range m.scripts {
if !d.Dirty() {
continue
}
if err := write(filepath.Base(m.scriptRecoveryFile(d.Path)), d.Code.Bytes()); err != nil {
return output, count, err
}
count++
}
if count == 0 {
return "", 0, fmt.Errorf("there are no unsaved documents to export")
}
note := "Aego emergency recovery copies from project: " + m.opt.Root + "\n" +
"These files are copies of unsaved documents, not original assets. " +
"Their names correspond to the project's .aego/recovery directory. They are not restored automatically. " +
"Keep existing recovery files, verify the source project and resolve conflicts before any manual restoration. " +
"Earlier pending recovery files were not copied by this operation.\n"
if err := write("README.txt", []byte(note)); err != nil {
return output, count, err
}
// A file flush alone does not persist the directory entries. This is still
// not a proof of power-loss safety, especially on unsupported platforms.
if runtime.GOOS != "windows" && runtime.GOOS != "js" && runtime.GOOS != "wasip1" {
for _, dir := range []string{name, "."} {
f, err := root.Open(dir)
if err != nil {
return output, count, err
}
if err := errors.Join(f.Sync(), f.Close()); err != nil {
return output, count, err
}
}
}
return output, count, nil
}