aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs
blob: f098def10cc2d84cebbdd04ea4be48f8f0143332 (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
/*
* Copyright (c) 2024 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.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore;

using VNLib.Utils;
using VNLib.Utils.Async;

namespace VNLib.Plugins.Extensions.Data.Storage
{

    /// <summary>
    /// Provides single table database object storage services
    /// </summary>
    public sealed class LWStorageManager : IAsyncResourceStateHandler
    { 
       
        /// <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)
        {
            ArgumentException.ThrowIfNullOrWhiteSpace(userId);
            
            //If no override id was specified, generate a new one
            descriptorIdOverride ??= NewDescriptorIdGenerator();

            DateTime createdOrModifedTime = DateTime.UtcNow;

            await using LWStorageContext ctx = GetContext();

            //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.SaveAndCloseAsync(true, cancellation);

            return result
                ? new LWStorageDescriptor(this, entry)
                : throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved");
        }

        /// <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)
        {
            ArgumentException.ThrowIfNullOrWhiteSpace(userid);

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

            await db.SaveAndCloseAsync(true, cancellation);

            //Close transactions and return
            return entry == null ? null : 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)
        {
            ArgumentException.ThrowIfNullOrWhiteSpace(descriptorId);
           
            //Init db
            await using LWStorageContext db = GetContext();           
            
            //Get entry
            LWStorageEntry? entry = await (from s in db.Descriptors
                                           where s.Id == descriptorId
                                           select s)
                                           .SingleOrDefaultAsync(cancellation);

            await db.SaveAndCloseAsync(true, cancellation);

            //Close transactions and return
            return entry == null ? null : 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();

            //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);

            //Commit transaction
            return await db.SaveAndCloseAsync(true, cancellation);
        }
       
        async Task IAsyncResourceStateHandler.UpdateAsync(AsyncUpdatableResource resource, object state, CancellationToken cancellation)
        {
            LWStorageEntry entry = (state as LWStorageEntry)!;
            ERRNO result = 0;
            try
            {
                await using LWStorageContext ctx = GetContext();

                //Begin tracking
                ctx.Descriptors.Attach(entry);
                
                //Update modified time
                entry.LastModified = DateTime.UtcNow;

                //Save changes
                result = await ctx.SaveAndCloseAsync(true, cancellation);
            }
            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");
            }
        }

        async Task IAsyncResourceStateHandler.DeleteAsync(AsyncUpdatableResource resource, CancellationToken cancellation)
        {
            LWStorageEntry descriptor = (resource as LWStorageDescriptor)!.Entry;
            ERRNO result;
            try
            {
                //Init db
                await using LWStorageContext db = GetContext();

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

                //Save changes and commit if successful
                result = await db.SaveAndCloseAsync(true, cancellation);
            }
            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");
            }
        }
    }
}