Skip to content
quarry Artifact [f7388fe203]
Login

Artifact [f7388fe203]

Artifact f7388fe203ca49b46ab391e43ae63f665c4d7807595fdebfc6343bf55e7d3cfb:


// chatfile.go is the files a chat message carries: an image pasted
// into the box, a file attached to the line. The bytes are in the
// repository's bucket under their hash, as every upload's are; this is
// which channel holds them, under what name, and from which message.
//
// A chat line is not in the repository's history and does not outlive
// the channel's retention, so neither does its file: a row goes when
// its message is deleted or ages out of the channel.
package oltp

import (
	"database/sql"
	"errors"
)

const chatFileSchema = `
CREATE TABLE IF NOT EXISTS chat_file (
	repo    TEXT NOT NULL,
	seq     INTEGER NOT NULL,        -- the message that carries it
	name    TEXT NOT NULL,           -- what its attach: names
	size    INTEGER NOT NULL,
	type    TEXT NOT NULL,
	sha256  TEXT NOT NULL,           -- the object's key in the bucket
	who     TEXT NOT NULL,
	created TEXT NOT NULL,
	PRIMARY KEY (repo, seq, name)
);
CREATE INDEX IF NOT EXISTS chat_file_sha ON chat_file(sha256);`

// ChatFile is one file a chat message carries.
type ChatFile struct {
	Repo    string `json:"repo"`
	Name    string `json:"name"`
	Size    int64  `json:"size"`
	Type    string `json:"type"`
	SHA256  string `json:"sha256"`
	Who     string `json:"who"`
	Seq     uint64 `json:"seq"`
	Created string `json:"created"`
}

// AddChatFile records a file against the message seq in repo's channel.
// A message names each of its files once; the same name again is the
// file replaced.
func (d *DB) AddChatFile(f ChatFile) error {
	if f.Repo == "" || f.Name == "" || f.SHA256 == "" || f.Seq == 0 {
		return errors.New("oltp: a chat file needs a repository, a name, a hash and its message")
	}
	_, err := d.w.Exec(`INSERT OR REPLACE INTO chat_file (repo, seq, name, size, type, sha256, who, created)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, f.Repo, f.Seq, f.Name, f.Size, f.Type, f.SHA256, f.Who, now())
	return err
}

const chatFileCols = `repo, seq, name, size, type, sha256, who, created`

func scanChatFile(row interface{ Scan(...any) error }) (ChatFile, error) {
	var f ChatFile
	err := row.Scan(&f.Repo, &f.Seq, &f.Name, &f.Size, &f.Type, &f.SHA256, &f.Who, &f.Created)
	return f, err
}

// ChatFile is the file named name that message seq in repo's channel
// carries; ok is false when it carries none.
func (d *DB) ChatFile(repo string, seq uint64, name string) (ChatFile, bool, error) {
	f, err := scanChatFile(d.db.QueryRow(`SELECT `+chatFileCols+` FROM chat_file WHERE repo = ? AND seq = ? AND name = ?`, repo, seq, name))
	if errors.Is(err, sql.ErrNoRows) {
		return ChatFile{}, false, nil
	}
	return f, err == nil, err
}

// DropChatFiles forgets the files the message seq carried, answering
// them so their bytes can go too.
func (d *DB) DropChatFiles(repo string, seq uint64) ([]ChatFile, error) {
	return d.dropChatFiles(`WHERE repo = ? AND seq = ?`, repo, seq)
}

// PruneChatFiles forgets the files of messages before first, the
// oldest sequence the channel still holds, answering them: the stream's
// retention took the messages, and their files go with them.
func (d *DB) PruneChatFiles(repo string, first uint64) ([]ChatFile, error) {
	return d.dropChatFiles(`WHERE repo = ? AND seq < ?`, repo, first)
}

func (d *DB) dropChatFiles(where string, args ...any) ([]ChatFile, error) {
	rows, err := d.db.Query(`SELECT `+chatFileCols+` FROM chat_file `+where, args...)
	if err != nil {
		return nil, err
	}
	var out []ChatFile
	for rows.Next() {
		f, err := scanChatFile(rows)
		if err != nil {
			rows.Close()
			return nil, err
		}
		out = append(out, f)
	}
	rows.Close()
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(out) == 0 {
		return nil, nil
	}
	_, err = d.w.Exec(`DELETE FROM chat_file `+where, args...)
	return out, err
}

// ChatFileShared says another chat file still names the bytes sha.
func (d *DB) ChatFileShared(sha string) bool {
	var n int
	_ = d.db.QueryRow(`SELECT COUNT(*) FROM chat_file WHERE sha256 = ?`, sha).Scan(&n)
	return n > 0
}