aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs
blob: 4120a8b6098e02b7ca73fa6ed51203b3ca5ab283 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/*
* Copyright (c) 2022 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
* File: LWStorageManager.cs 
*
* LWStorageManager.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify 
* it under the terms of the GNU Affero General Public License as 
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see https://www.gnu.org/licenses/.
*/

using System;
using System.IO;
using System.Data;
using System.Linq;
using System.Threading;
using System.IO.Compression;
using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore;

using VNLib.Utils;
using VNLib.Utils.IO;
using VNLib.Utils.Memory;

namespace VNLib.Plugins.Extensions.Data.Storage
{

    /// <summary>
    /// Provides single table database object storage services
    /// </summary>
    public sealed class LWStorageManager
    { 
        /// <summary>
        /// The generator function that is invoked when a new <see cref="LWStorageDescriptor"/> is to 
        /// be created without an explicit id
        /// </summary>
        public Func<string> NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N");

        private readonly DbContextOptions DbOptions;
        private readonly string TableName;

        private LWStorageContext GetContext() => new(DbOptions, TableName);

        /// <summary>
        /// Creates a new <see cref="LWStorageManager"/> with 
        /// </summary>
        /// <param name="options">The db context options to create database connections with</param>
        /// <param name="tableName">The name of the table to operate on</param>
        /// <exception cref="ArgumentNullException"></exception>
        public LWStorageManager(DbContextOptions options, string tableName)
        {
            DbOptions = options ?? throw new ArgumentNullException(nameof(options));
            TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
        }

        /// <summary>
        /// Creates a new <see cref="LWStorageDescriptor"/> fror a given user
        /// </summary>
        /// <param name="userId">Id of user</param>
        /// <param name="descriptorIdOverride">An override to specify the new descriptor's id</param>
        /// <param name="cancellation">A token to cancel the operation</param>
        /// <returns>A new <see cref="LWStorageDescriptor"/> if successfully created, null otherwise</returns>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="LWDescriptorCreationException"></exception>
        public async Task<LWStorageDescriptor> CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default)
        {
            if (string.IsNullOrWhiteSpace(userId))
            {
                throw new ArgumentNullException(nameof(userId));
            }
            
            //If no override id was specified, generate a new one
            descriptorIdOverride ??= NewDescriptorIdGenerator();

            DateTime createdOrModifedTime = DateTime.UtcNow;

            await using LWStorageContext ctx = GetContext();
            await ctx.OpenTransactionAsync(cancellation);

            //Make sure the descriptor doesnt exist only by its descriptor id
            if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation))
            {
                throw new LWDescriptorCreationException($"A descriptor with id {descriptorIdOverride} already exists");
            }

            //Cache time
            DateTime now = DateTime.UtcNow;

            //Create the new descriptor
            LWStorageEntry entry = new()
            {
                Created = now,
                LastModified = now,
                Id = descriptorIdOverride,
                UserId = userId,
            };

            //Add and save changes
            ctx.Descriptors.Add(entry);

            ERRNO result = await ctx.SaveChangesAsync(cancellation);

            if (!result)
            {
                //Rollback and raise exception
                await ctx.RollbackTransctionAsync(cancellation);
                throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved");
            }
            else
            {
                //Commit transaction and return the new descriptor
                await ctx.CommitTransactionAsync(cancellation);
                return new LWStorageDescriptor(this, entry);
            }
        }

        /// <summary>
        /// Attempts to retrieve <see cref="LWStorageDescriptor"/> for a given user-id. The caller is responsible for 
        /// consitancy state of the descriptor
        /// </summary>
        /// <param name="userid">User's id</param>
        /// <param name="cancellation">A token to cancel the operation</param>
        /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public async Task<LWStorageDescriptor?> GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default)
        {
            //Allow null/empty entrys to just return null
            if (string.IsNullOrWhiteSpace(userid))
            {
                throw new ArgumentNullException(nameof(userid));
            }

            //Init db
            await using LWStorageContext db = GetContext();
            //Begin transaction
            await db.OpenTransactionAsync(cancellation);
            //Get entry
            LWStorageEntry? entry = await (from s in db.Descriptors
                                           where s.UserId == userid
                                           select s)
                                           .SingleOrDefaultAsync(cancellation);

            //Close transactions and return
            if (entry == null)
            {
                await db.RollbackTransctionAsync(cancellation);
                return null;
            }
            else
            {
                await db.CommitTransactionAsync(cancellation);
                return new (this, entry);
            }
        }
        
        /// <summary>
        /// Attempts to retrieve the <see cref="LWStorageDescriptor"/> for the given descriptor id. The caller is responsible for 
        /// consitancy state of the descriptor
        /// </summary>
        /// <param name="descriptorId">Unique identifier for the descriptor</param>
        /// <param name="cancellation">A token to cancel the opreeaiton</param>
        /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public async Task<LWStorageDescriptor?> GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default)
        {
            //Allow null/empty entrys to just return null
            if (string.IsNullOrWhiteSpace(descriptorId))
            {
                throw new ArgumentNullException(nameof(descriptorId));
            }
           
            //Init db
            await using LWStorageContext db = GetContext();
            //Begin transaction
            await db.OpenTransactionAsync(cancellation);
            //Get entry
            LWStorageEntry? entry = await (from s in db.Descriptors
                                           where s.Id == descriptorId
                                           select s)
                                           .SingleOrDefaultAsync(cancellation);

            //Close transactions and return
            if (entry == null)
            {
                await db.RollbackTransctionAsync(cancellation);
                return null;
            }
            else
            {
                await db.CommitTransactionAsync(cancellation);
                return new (this, entry);
            }
        }
       
        /// <summary>
        /// Cleanup entries before the specified <see cref="TimeSpan"/>. Entires are store in UTC time
        /// </summary>
        /// <param name="compareTime">Time before <see cref="DateTime.UtcNow"/> to compare against</param>
        /// <param name="cancellation">A token to cancel the operation</param>
        /// <returns>The number of entires cleaned</returns>S
        public Task<ERRNO> CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation);
        
        /// <summary>
        /// Cleanup entries before the specified <see cref="DateTime"/>. Entires are store in UTC time
        /// </summary>
        /// <param name="compareTime">UTC time to compare entires against</param>
        /// <param name="cancellation">A token to cancel the operation</param>
        /// <returns>The number of entires cleaned</returns>
        public async Task<ERRNO> CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default)
        {
            //Init db
            await using LWStorageContext db = GetContext();
            //Begin transaction
            await db.OpenTransactionAsync(cancellation);

            //Get all expired entires
            LWStorageEntry[] expired = await (from s in db.Descriptors
                                              where s.Created < compareTime
                                              select s)
                                              .ToArrayAsync(cancellation);

            //Delete
            db.Descriptors.RemoveRange(expired);

            //Save changes
            ERRNO count = await db.SaveChangesAsync(cancellation);

            //Commit transaction
            await db.CommitTransactionAsync(cancellation);

            return count;
        }

        /// <summary>
        /// Updates a descriptor's data field
        /// </summary>
        /// <param name="descriptorObj">Descriptor to update</param>
        /// <param name="data">Data string to store to descriptor record</param>
        /// <exception cref="LWStorageUpdateFailedException"></exception>
        internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data)
        {
            LWStorageEntry entry = (descriptorObj as LWStorageDescriptor)!.Entry;
            ERRNO result = 0;
            try
            {
                await using LWStorageContext ctx = GetContext();
                await ctx.OpenTransactionAsync(CancellationToken.None);

                //Begin tracking
                ctx.Descriptors.Attach(entry);

                //Convert stream to vnstream
                VnMemoryStream vms = (VnMemoryStream)data;
                using (IMemoryHandle<byte> encBuffer = Memory.SafeAlloc<byte>((int)vms.Length))
                {
                    //try to compress
                    if(!BrotliEncoder.TryCompress(vms.AsSpan(), encBuffer.Span, out int compressed))
                    {
                        throw new InvalidDataException("Failed to compress the descriptor data");
                    }
                    //Set the data 
                    entry.Data = encBuffer.Span.ToArray();
                }
                //Update modified time
                entry.LastModified = DateTime.UtcNow;

                //Save changes
                result = await ctx.SaveChangesAsync(CancellationToken.None);

                //Commit or rollback
                if (result)
                {
                    await ctx.CommitTransactionAsync(CancellationToken.None);
                }
                else
                {
                    await ctx.RollbackTransctionAsync(CancellationToken.None);
                }
            }
            catch (Exception ex)
            {
                throw new LWStorageUpdateFailedException("", ex);
            }
            //If the result is 0 then the update failed
            if (!result)
            {
                throw new LWStorageUpdateFailedException($"Descriptor {entry.Id} failed to update");
            }
        }
        
        /// <summary>
        /// Function to remove the specified descriptor 
        /// </summary>
        /// <param name="descriptorObj">The active descriptor to remove from the database</param>
        /// <exception cref="LWStorageRemoveFailedException"></exception>
        internal async Task RemoveDescriptorAsync(object descriptorObj)
        {
            LWStorageEntry descriptor = (descriptorObj as LWStorageDescriptor)!.Entry;
            ERRNO result;
            try
            {
                //Init db
                await using LWStorageContext db = GetContext();
                //Begin transaction
                await db.OpenTransactionAsync();

                //Delete the user from the database
                db.Descriptors.Remove(descriptor);

                //Save changes and commit if successful
                result = await db.SaveChangesAsync();

                if (result)
                {
                    await db.CommitTransactionAsync();
                }
                else
                {
                    await db.RollbackTransctionAsync();
                }
            }
            catch (Exception ex)
            {
                throw new LWStorageRemoveFailedException("", ex);
            }
            if (!result)
            {
                throw new LWStorageRemoveFailedException("Failed to delete the user account because of a database failure, the user may already be deleted");
            }
        }
        
    }
}