Skip to content

Commit df044ec

Browse files
authored
Sync File Attachments (#68)
Signed-off-by: Austin Hale <ahale@nakamir.com>
1 parent ad7c5ce commit df044ec

16 files changed

Lines changed: 2157 additions & 0 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace PowerSync.Common.Attachments;
2+
3+
using PowerSync.Common.DB.Schema.Attributes;
4+
5+
/// <summary>
6+
/// An attachment record persisted in the local database.
7+
/// </summary>
8+
[Table(TableName, LocalOnly = true)]
9+
public sealed class Attachment
10+
{
11+
/// <summary>The attachment table name.</summary>
12+
public const string TableName = "attachments";
13+
14+
[Column("id")]
15+
public string Id { get; set; } = string.Empty;
16+
17+
[Column("filename")]
18+
public string Filename { get; set; } = string.Empty;
19+
20+
[Column("state")]
21+
public AttachmentState State { get; set; }
22+
23+
[Column("local_uri")]
24+
public string? LocalUri { get; set; }
25+
26+
[Column("size")]
27+
public long? Size { get; set; }
28+
29+
[Column("media_type")]
30+
public string? MediaType { get; set; }
31+
32+
[Column("timestamp")]
33+
public long Timestamp { get; set; }
34+
35+
[Column("meta_data")]
36+
public string? MetaData { get; set; }
37+
38+
[Column("has_synced")]
39+
public bool HasSynced { get; set; }
40+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
namespace PowerSync.Common.Attachments;
2+
3+
using Microsoft.Extensions.Logging;
4+
5+
using Newtonsoft.Json;
6+
7+
using PowerSync.Common.Client;
8+
using PowerSync.Common.DB;
9+
10+
/// <summary>
11+
/// Database operations for managing attachment records.
12+
/// Provides query, insert, update and delete primitives with transaction-aware overloads.
13+
/// </summary>
14+
internal sealed class AttachmentContext(IPowerSyncDatabase db, string tableName, int maxArchivedCount, ILogger logger)
15+
{
16+
/// <summary>The PowerSync database used for queries.</summary>
17+
public IPowerSyncDatabase Db => db;
18+
19+
public Task DeleteAttachmentAsync(string id) => db.WriteTransaction(tx => tx.Execute(
20+
$"DELETE FROM {tableName} WHERE id = ?",
21+
[id]));
22+
23+
public Task IgnoreAttachmentAsync(string id) => db.Execute(
24+
$"UPDATE {tableName} SET state = ? WHERE id = ?",
25+
[(int)AttachmentState.Archived, id]);
26+
27+
public Task<Attachment?> GetAttachmentAsync(string id) => db.GetOptional<Attachment>(
28+
$"SELECT * FROM {tableName} WHERE id = ?",
29+
[id]);
30+
31+
public Task<Attachment> SaveAttachmentAsync(Attachment attachment) => db.WriteLock(async ctx =>
32+
{
33+
await UpsertAttachmentAsync(attachment, ctx);
34+
return attachment;
35+
});
36+
37+
public Task SaveAttachmentsAsync(IReadOnlyList<Attachment> attachments)
38+
{
39+
if (attachments.Count == 0)
40+
{
41+
logger.LogDebug("No attachments to save.");
42+
return Task.CompletedTask;
43+
}
44+
45+
return db.WriteTransaction(async tx =>
46+
{
47+
foreach (var attachment in attachments)
48+
{
49+
await UpsertAttachmentAsync(attachment, tx);
50+
}
51+
});
52+
}
53+
54+
public Task<string[]> GetAttachmentIdsAsync() => db.GetAll<string>(
55+
$"SELECT id FROM {tableName} WHERE id IS NOT NULL");
56+
57+
public Task<Attachment[]> GetAttachmentsAsync() => db.GetAll<Attachment>(
58+
$@"
59+
SELECT *
60+
FROM {tableName}
61+
ORDER BY timestamp ASC
62+
");
63+
64+
public Task<Attachment[]> GetActiveAttachmentsAsync() => db.GetAll<Attachment>(
65+
$@"
66+
SELECT *
67+
FROM {tableName}
68+
WHERE state != ?
69+
ORDER BY timestamp ASC
70+
",
71+
[(int)AttachmentState.Archived]);
72+
73+
public Task ClearQueueAsync() => db.WriteTransaction(tx => tx.Execute(
74+
$"DELETE FROM {tableName}"));
75+
76+
public async Task<bool> DeleteArchivedAttachmentsAsync(Func<Attachment[], Task>? callback = null, int limit = 1000)
77+
{
78+
var archived = await db.GetAll<Attachment>(
79+
$@"
80+
SELECT *
81+
FROM {tableName}
82+
WHERE state = ?
83+
ORDER BY timestamp DESC
84+
LIMIT ?
85+
OFFSET ?
86+
",
87+
[(int)AttachmentState.Archived, limit, maxArchivedCount]);
88+
89+
if (archived.Length == 0)
90+
{
91+
return true;
92+
}
93+
94+
logger.LogInformation(
95+
"Deleting {Count} archived attachments, (exceeding maxArchivedCount={MaxArchivedCount})...",
96+
archived.Length,
97+
maxArchivedCount);
98+
99+
// Call the callback with the list of archived attachments before deletion.
100+
if (callback is not null)
101+
{
102+
await callback(archived);
103+
}
104+
105+
// Delete the archived attachments from the table.
106+
var ids = archived.Select(a => a.Id).ToArray();
107+
await db.Execute(
108+
$"DELETE FROM {tableName} WHERE id IN (SELECT json_each.value FROM json_each(?))",
109+
[JsonConvert.SerializeObject(ids)]);
110+
111+
logger.LogInformation("Deleted {Count} archived attachments", archived.Length);
112+
return archived.Length < limit;
113+
}
114+
115+
public Task UpsertAttachmentAsync(Attachment attachment, ILockContext ctx)
116+
{
117+
logger.LogDebug("Updating attachment {Id}: {State}", attachment.Id, attachment.State);
118+
119+
return ctx.Execute(
120+
$@"
121+
INSERT OR REPLACE INTO {tableName} (
122+
id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data
123+
)
124+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
125+
",
126+
[
127+
attachment.Id,
128+
attachment.Timestamp,
129+
attachment.Filename,
130+
attachment.LocalUri,
131+
attachment.MediaType,
132+
attachment.Size,
133+
(int)attachment.State,
134+
attachment.HasSynced,
135+
attachment.MetaData,
136+
]);
137+
}
138+
}

0 commit comments

Comments
 (0)