sync app filled in
This commit is contained in:
@@ -38,6 +38,7 @@ type AttachmentRecord struct {
|
||||
ContentType string
|
||||
SizeBytes int64
|
||||
CreatedAt time.Time
|
||||
DocumentPath string
|
||||
}
|
||||
|
||||
type PersistDocumentInput struct {
|
||||
@@ -238,13 +239,14 @@ func (r *Repository) UpsertUser(ctx context.Context, user UserRecord) (UserRecor
|
||||
|
||||
func (r *Repository) SaveAttachment(ctx context.Context, record AttachmentRecord) error {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO attachments (hash, original_name, content_type, size_bytes, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO attachments (hash, original_name, content_type, size_bytes, created_at, document_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(hash) DO UPDATE SET
|
||||
original_name = excluded.original_name,
|
||||
content_type = excluded.content_type,
|
||||
size_bytes = excluded.size_bytes
|
||||
`, record.Hash, record.OriginalName, record.ContentType, record.SizeBytes, record.CreatedAt.Format(time.RFC3339)); err != nil {
|
||||
size_bytes = excluded.size_bytes,
|
||||
document_path = excluded.document_path
|
||||
`, record.Hash, record.OriginalName, record.ContentType, record.SizeBytes, record.CreatedAt.Format(time.RFC3339), record.DocumentPath); err != nil {
|
||||
return fmt.Errorf("save attachment: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -256,10 +258,10 @@ func (r *Repository) GetAttachment(ctx context.Context, hash string) (*Attachmen
|
||||
created string
|
||||
)
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT hash, original_name, content_type, size_bytes, created_at
|
||||
SELECT hash, original_name, content_type, size_bytes, created_at, COALESCE(document_path, '')
|
||||
FROM attachments
|
||||
WHERE hash = ?
|
||||
`, hash).Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created)
|
||||
`, hash).Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created, &record.DocumentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -273,7 +275,7 @@ func (r *Repository) GetAttachment(ctx context.Context, hash string) (*Attachmen
|
||||
|
||||
func (r *Repository) ListAttachments(ctx context.Context) ([]AttachmentRecord, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT hash, original_name, content_type, size_bytes, created_at
|
||||
SELECT hash, original_name, content_type, size_bytes, created_at, COALESCE(document_path, '')
|
||||
FROM attachments
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
@@ -286,7 +288,7 @@ func (r *Repository) ListAttachments(ctx context.Context) ([]AttachmentRecord, e
|
||||
for rows.Next() {
|
||||
var record AttachmentRecord
|
||||
var created string
|
||||
if err := rows.Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created); err != nil {
|
||||
if err := rows.Scan(&record.Hash, &record.OriginalName, &record.ContentType, &record.SizeBytes, &created, &record.DocumentPath); err != nil {
|
||||
return nil, fmt.Errorf("scan attachment: %w", err)
|
||||
}
|
||||
record.CreatedAt, err = time.Parse(time.RFC3339, created)
|
||||
|
||||
@@ -190,7 +190,17 @@ func (s *Service) SaveSourcePageWithBaseHash(ctx context.Context, requestPath st
|
||||
|
||||
record, _, err := s.resolveRecord(ctx, requestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if baseHash != "" {
|
||||
return nil, err
|
||||
}
|
||||
path, pathErr := normalizeWritableDocumentPath(requestPath)
|
||||
if pathErr != nil {
|
||||
return nil, pathErr
|
||||
}
|
||||
record = &DocumentRecord{Path: path}
|
||||
}
|
||||
|
||||
if baseHash != "" && record.CurrentHash != baseHash {
|
||||
@@ -270,6 +280,21 @@ func normalizeRequestPathCandidates(path string) []string {
|
||||
return []string{path}
|
||||
}
|
||||
|
||||
func normalizeWritableDocumentPath(path string) (string, error) {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("document path is required")
|
||||
}
|
||||
path = filepath.ToSlash(filepath.Clean(filepath.FromSlash(path)))
|
||||
if path == "." || strings.HasPrefix(path, "../") || path == ".." || filepath.IsAbs(path) {
|
||||
return "", fmt.Errorf("invalid document path")
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(path), ".md") {
|
||||
path += ".md"
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *Service) syncFile(ctx context.Context, path string) (*DocumentChange, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -150,6 +150,38 @@ func TestSaveSourcePageWithBaseHashCanResolveWithLatestHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveSourcePageCreatesNewDocument(t *testing.T) {
|
||||
service, sourceDir := setupDocsTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
content := "# Inbox Note\n\nCreated from clipboard"
|
||||
page, err := service.SaveSourcePageWithBaseHash(ctx, "inbox/note", content, "")
|
||||
if err != nil {
|
||||
t.Fatalf("SaveSourcePageWithBaseHash() error = %v", err)
|
||||
}
|
||||
if page.Path != "inbox/note.md" {
|
||||
t.Fatalf("page path = %q, want inbox/note.md", page.Path)
|
||||
}
|
||||
|
||||
written, err := os.ReadFile(filepath.Join(sourceDir, "inbox", "note.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read new document: %v", err)
|
||||
}
|
||||
if string(written) != content {
|
||||
t.Fatalf("new document content = %q, want %q", string(written), content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveSourcePageRejectsTraversalForNewDocument(t *testing.T) {
|
||||
service, _ := setupDocsTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := service.SaveSourcePageWithBaseHash(ctx, "../outside", "# Outside\n", "")
|
||||
if err == nil {
|
||||
t.Fatal("expected traversal path to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSourceDirHandles100FilesUnderTarget(t *testing.T) {
|
||||
service, sourceDir := setupDocsTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user