main pico / pkg / send / protocols / sftp / writer.go
Antonio Mika  ·  2026-10-10
  1package sftp
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"sync"
  7	"time"
  8
  9	"github.com/picosh/pico/pkg/send/utils"
 10)
 11
 12type buffer struct {
 13	buf []byte
 14	m   sync.Mutex
 15}
 16
 17func (b *buffer) WriteAt(p []byte, pos int64) (n int, err error) {
 18	pLen := len(p)
 19	expLen := pos + int64(pLen)
 20	b.m.Lock()
 21	defer b.m.Unlock()
 22	if int64(len(b.buf)) < expLen {
 23		oldLen := len(b.buf)
 24		if int64(cap(b.buf)) < expLen {
 25			newBuf := make([]byte, expLen)
 26			copy(newBuf, b.buf)
 27			b.buf = newBuf
 28		}
 29		// A truncated buffer keeps its old bytes past its length.
 30		b.buf = b.buf[:expLen]
 31		clear(b.buf[oldLen:])
 32	}
 33	copy(b.buf[pos:], p)
 34	return pLen, nil
 35}
 36
 37func (b *buffer) truncate(size int64) {
 38	b.m.Lock()
 39	defer b.m.Unlock()
 40	if size < int64(len(b.buf)) {
 41		b.buf = b.buf[:size]
 42		return
 43	}
 44	b.buf = append(b.buf, make([]byte, size-int64(len(b.buf)))...)
 45}
 46
 47func (b *buffer) bytes() []byte {
 48	b.m.Lock()
 49	defer b.m.Unlock()
 50	return bytes.Clone(b.buf)
 51}
 52
 53// pendingFile is a file open for writing. Editors over sshfs stat a file
 54// right after creating it, so it is listed from here until the last handle
 55// closes and its contents are written.
 56type pendingFile struct {
 57	path    string
 58	entry   *utils.FileEntry
 59	buf     *buffer
 60	modTime time.Time
 61	handles int
 62	// stored is set when storage had the file at open. Unless a handle
 63	// truncated or wrote to it, it's left as it is.
 64	stored bool
 65	dirty  bool
 66	// replaces holds stored files this one was renamed from while open.
 67	// They are deleted once it is stored at its new path.
 68	replaces []renamedFile
 69}
 70
 71// renamedFile is a stored file renamed away, with the checksum it had then.
 72// Another session may replace it before it's deleted.
 73type renamedFile struct {
 74	path string
 75	sum  []byte
 76}
 77
 78func (p *pendingFile) info(name string) *utils.VirtualFile {
 79	return &utils.VirtualFile{
 80		FName:    name,
 81		FSize:    int64(len(p.buf.bytes())),
 82		FModTime: p.modTime,
 83	}
 84}
 85
 86type pendingWriter struct {
 87	handler *handler
 88	file    *pendingFile
 89}
 90
 91func (w *pendingWriter) WriteAt(p []byte, off int64) (int, error) {
 92	w.handler.mu.Lock()
 93	w.file.dirty = true
 94	w.handler.mu.Unlock()
 95	return w.file.buf.WriteAt(p, off)
 96}
 97
 98func (w *pendingWriter) Close() error {
 99	h, f := w.handler, w.file
100	h.mu.Lock()
101	f.handles--
102	// A file removed while open is dropped.
103	commit := f.handles == 0 && h.pending[f.path] == f
104	if commit {
105		delete(h.pending, f.path)
106	}
107	name := f.path
108	h.mu.Unlock()
109	if !commit || (f.stored && !f.dirty) {
110		return nil
111	}
112
113	data := f.buf.bytes()
114	entry := *f.entry
115	entry.Filepath = name
116	entry.Reader = bytes.NewReader(data)
117	entry.Size = int64(len(data))
118	msg, err := h.writeHandler.Write(h.session, &entry)
119	h.mu.Lock()
120	replaced := h.release(f)
121	h.mu.Unlock()
122	if err != nil {
123		_, _ = fmt.Fprintf(h.session.Stderr(), "%s\r\n", err)
124		return err
125	}
126	if msg != "" {
127		_, _ = fmt.Fprintf(h.session.Stderr(), "%s\r\n", msg)
128	}
129	for _, old := range replaced {
130		if err := h.deleteIfUnchanged(old.path, old.sum); err != nil {
131			_, _ = fmt.Fprintf(h.session.Stderr(), "%s\r\n", err)
132			return err
133		}
134	}
135	return nil
136}
137
138// ignoredWriter accepts a file the handler ignores by policy, so a
139// transfer moves on to the next one.
140type ignoredWriter struct {
141	handler *handler
142	path    string
143}
144
145func (ignoredWriter) WriteAt(p []byte, _ int64) (int, error) { return len(p), nil }
146
147func (w ignoredWriter) Close() error {
148	w.handler.mu.Lock()
149	defer w.handler.mu.Unlock()
150	w.handler.ignored[w.path]--
151	if w.handler.ignored[w.path] <= 0 {
152		delete(w.handler.ignored, w.path)
153	}
154	return nil
155}