OpenNHP/opennhp · error

already exists, please delete it first

Error message

%v already exists, please delete it first

What it means

Save refuses to overwrite an existing data private key file. Before creating data-key-<doId>.json it runs os.Stat; if the file already exists it returns this error instructing the caller to delete it first. This is an intentional safety guard against silently replacing a ZTDO data private key, which would make previously wrapped data undecryptable.

Solutions

  1. If the existing key is still valid, load it with NewDataPrivateKeyStoreWith(doId) instead of generating a new one.
  2. If the key must be rotated, explicitly call Delete(doId) (or remove the file) first, understanding that data wrapped with the old key becomes undecryptable.
  3. Before saving, check existence with os.Stat on <exeDir>/etc/ztdo/data-key-<doId>.json and branch your logic accordingly.
  4. Fix duplicate doId generation if the same identifier is being reused across runs.

Example fix

// before: blind Save fails on second run
if err := store.Save(doId); err != nil { return err }
// after: only save when no key exists yet
path := filepath.Join(common.ExeDirPath, "etc/ztdo", "data-key-"+doId+".json")
if _, err := os.Stat(path); os.IsNotExist(err) {
	if err := store.Save(doId); err != nil { return err }
}
Defensive patterns

Strategy: validation

Validate before calling

path := filepath.Join(common.ExeDirPath, "etc/ztdo", "data-key-"+doId+".json")
exists := false
if _, err := os.Stat(path); err == nil { exists = true }
if exists {
	// load existing key instead of saving a new one
}

Type guard

func keyAlreadySaved(doId string) bool {
	_, err := os.Stat(filepath.Join(common.ExeDirPath, "etc/ztdo", "data-key-"+doId+".json"))
	return err == nil
}

Try / catch

err := store.Save(doId)
if err != nil && strings.Contains(err.Error(), "already exists") {
	store, err = db.NewDataPrivateKeyStoreWith(doId) // reuse existing key
}

Prevention

When it happens

Trigger: Calling Save(doId) when data-key-<doId>.json already exists in <exeDir>/etc/ztdo: (1) re-generating a key for a doId that already has one; (2) a leftover file from a previous run with the same doId; (3) calling Save twice in one flow.

Common situations: Re-running an init/registration step on an already-provisioned daemon; a doId collision (same UUID reused); operator retries a bootstrap script without cleaning up the first attempt's key file.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/1fd74737f1bc8ef4. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/db/utils.go:74

func (d *DataPrivateKeyStore) Generate(mode ztdolib.DataKeyPairECCMode) (privateKey []byte) {
	ecdh := core.NewECDH(mode.ToEccType())
	d.DataPrivateKeyBase64 = ecdh.PrivateKeyBase64()
	return ecdh.PrivateKey()
}

// Save saves the dataPrivateKeyBase64 to a file, the format of file name is data-<doId>.json
// Notes: this default way to store data private key is not safe. In the wild environment, need to use a secure way to store data private key.
func (d *DataPrivateKeyStore) Save(doId string) error {
	// Make sure the etc directory exists
	etcDir := "etc/ztdo"
	if err := os.MkdirAll(etcDir, 0755); err != nil {
		return fmt.Errorf("failed to create etc directory: %v", err)
	}

	fileName := "data-key-" + doId + ".json"
	fullPath := filepath.Join(common.ExeDirPath, etcDir, fileName)
	if _, err := os.Stat(fullPath); err == nil {
		return fmt.Errorf("%v already exists, please delete it first", fullPath)
	}

	file, err := os.Create(fullPath)
	if err != nil {
		return fmt.Errorf("failed to create file: %v", err)
	}
	defer file.Close()

	_, err = file.Write(d.toJson())
	return err
}

func (d *DataPrivateKeyStore) Delete(doId string) error {
	etcDir := "etc/ztdo"
	fileName := "data-key-" + doId + ".json"
	fullPath := filepath.Join(common.ExeDirPath, etcDir, fileName)

	// delete the file

View on GitHub (pinned to 6e04ca5ff0)