13 voiceDirName = "voices"
14 voiceMaxBytes = 20 << 20
17 var errVoiceSize = errors.New("voice note is empty or exceeds 20 MB")
19 // VoiceStore holds immutable audio shared by all agents. Names are hashes of
20 // the received bytes, not sender filenames. The decoder detects the format.
21 type VoiceStore struct {
25 // Save publishes private audio atomically before its message is acknowledged.
26 // Only nonempty input up to voiceMaxBytes is stored.
27 func (s *VoiceStore) Save(data []byte) (name string, err error) {
28 if len(data) == 0 || len(data) > voiceMaxBytes {
29 return "", errVoiceSize
32 sum := sha256.Sum256(data)
33 name = hex.EncodeToString(sum[:]) + ".audio"
34 err = os.MkdirAll(s.dir, 0o700)
36 return "", fmt.Errorf("create voice directory: %w", err)
39 file, err := os.CreateTemp(s.dir, ".voice-*")
41 return "", fmt.Errorf("create voice file: %w", err)
45 err = errors.Join(err, os.Remove(file.Name()))
48 _, err = file.Write(data)
53 err = errors.Join(err, file.Close())
55 return "", fmt.Errorf("write voice: %w", err)
58 err = os.Link(file.Name(), filepath.Join(s.dir, name))
59 if err != nil && !errors.Is(err, os.ErrExist) {
60 return "", fmt.Errorf("publish voice: %w", err)