GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GoogleDrive.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
18using System;
19using System.Collections.Generic;
20using System.IO;
21using System.Linq;
22using System.Net.Http;
23using static GenerallyPositive.Enums;
24
25namespace GenerallyPositive
26{
27 // Configuration class for Google Drive API settings
28 public class GoogleDriveConfig
29 {
30 public string DriveApiBase { get; set; } = "https://www.googleapis.com/drive";
31 public string FilesEndpoint { get; set; } = "/v3/files";
32 public string UploadEndpoint { get; set; } = "/upload/drive/v3/files";
33 public string AuthorizationHeader { get; set; } = "Authorization";
34 public string Bearer { get; set; } = "Bearer";
35 public string QueryFields { get; set; } = "id,name,mimeType,parents";
36 public string UploadType { get; set; } = "?uploadType=multipart";
37 }
38
39 // Interfaces for fluent API
40 public interface IAllowDriveCredentials
41 {
42 IAllowDriveFileSelection WithCredentials(ICredentials credentials);
43 }
44
45 public interface IAllowDriveFileSelection : IAllowToGPALObject<IGoogleDrive>
46 {
47 IAllowLocalFileOperations WithLocalFile(GPALFile file);
48 IAllowRemoteFileOperations WithFileId(string fileId);
49 IAllowRemoteFileOperations WithFileName(string name);
50 }
51
53 {
54 IAllowDriveActions UploadTo(string fileName);
55 }
56
58 {
59 IAllowDriveActions SaveTo(GPALFile destination);
60 IAllowDriveActions DeleteFile();
61 IAllowDriveActions MoveTo(string folderId);
62 IAllowDriveActions RenameTo(string newName);
63 IAllowDriveListOperations WithFileIds(out IGPALGrid<string> fileIds);
64 IAllowDriveListOperations ListFiles(out IGPALGrid<string> files);
65 }
66
68 {
69 }
70
74
78
79 // Main GoogleDrive class
80 public class GoogleDrive : IGoogleDrive
81 {
82 internal GoogleDriveConfig Config { get; private set; }
83 internal IRESTClient DriveRESTClient { get; private set; }
84 internal ICredentials Credentials { get; private set; }
85 internal GPALFile LocalFile { get; private set; }
86 internal string FileId { get; private set; }
87 internal string FileName { get; private set; }
88
89 internal GoogleDrive()
90 {
91 Config = new GoogleDriveConfig();
92 DriveRESTClient = GPAL.RESTClient.WithAPIBase(Config.DriveApiBase).ToGPALObject();
93 }
94
95 public IAllowDriveFileSelection WithCredentials(ICredentials credentials)
96 {
97 Credentials = credentials;
98 return this;
99 }
100
101 public IAllowLocalFileOperations WithLocalFile(GPALFile file)
102 {
103 LocalFile = file;
104 return this;
105 }
106
107 public IAllowRemoteFileOperations WithFileId(string fileId)
108 {
109 FileId = fileId;
110 return this;
111 }
112
113 public IAllowRemoteFileOperations WithFileName(string name)
114 {
115 FileName = name;
116 return this;
117 }
118
119 private string GetToken()
120 {
121 Credentials.FetchAccessToken(out string token);
122 return token;
123 }
124
125 private string ResolveFileId()
126 {
127 if (false == string.IsNullOrEmpty(FileId))
128 return FileId;
129
130 if (string.IsNullOrEmpty(FileName))
131 return null;
132
133 var query = $"name='{FileName}' and trashed=false";
134 var response = DriveRESTClient
135 .WithEndpoint($"{Config.FilesEndpoint}?q={Uri.EscapeDataString(query)}&fields=files(id)")
136 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
137 .Execute<DriveFilesResponse>();
138
139 return response?.Files?.FirstOrDefault()?.Id;
140 }
141
142 public IAllowDriveActions UploadTo(string fileName)
143 {
144 if (LocalFile == null || LocalFile.Filenames.Count == 0)
145 {
146 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No local file specified for upload", null, GPALObjectType.None);
147 return this;
148 }
149
150 string filePath = LocalFile.Filenames.First();
151
152 FileName = fileName;
153 string existingId = ResolveFileId();
154 if (existingId != null)
155 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"A file named [{fileName}] already exists in Drive (id: [{existingId}]). Drive will create a duplicate.", null, GPALObjectType.None);
156 FileName = null;
157
158 try
159 {
160 var content = new MultipartFormDataContent();
161 content.Add(new StringContent($"{{ \"name\": \"{fileName}\" }}"), "metadata", "application/json");
162 content.Add(new ByteArrayContent(File.ReadAllBytes(filePath)), "file", fileName);
163
164 var response = DriveRESTClient
165 .WithEndpoint($"{Config.UploadEndpoint}{Config.UploadType}")
166 .WithParameters(content)
167 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
168 .Execute<DriveFileResponse>();
169
170 if (response == null)
171 {
172 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to upload file [{filePath}]", null, GPALObjectType.None);
173 }
174 else
175 {
176 FileId = response.Id;
177 }
178 }
179 catch (Exception ex)
180 {
181 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to upload file [{filePath}]", null, GPALObjectType.None, ex);
182 }
183
184 return this;
185 }
186
187 public IAllowDriveActions SaveTo(GPALFile destination)
188 {
189 var resolvedId = ResolveFileId();
190 if (string.IsNullOrEmpty(resolvedId))
191 {
192 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for download", null, GPALObjectType.None);
193 return this;
194 }
195
196 if (destination == null || destination.Filenames.Count == 0)
197 {
198 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No destination file specified for download", null, GPALObjectType.None);
199 return this;
200 }
201
202 string destinationPath = destination.Filenames.First();
203 try
204 {
205 var bytes = DriveRESTClient
206 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?alt=media")
207 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
208 .Execute<byte[]>();
209
210 if (bytes != null)
211 {
212 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{bytes.Length}] bytes of drive file [{resolvedId}] to [{destinationPath}].", null, GPALObjectType.None);
213 File.WriteAllBytes(destinationPath, bytes);
214 }
215 else
216 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to download file [{resolvedId}]", null, GPALObjectType.None);
217 }
218 catch (Exception ex)
219 {
220 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to download file [{resolvedId}]", null, GPALObjectType.None, ex);
221 }
222
223 return this;
224 }
225
226 public IAllowDriveActions DeleteFile()
227 {
228 var resolvedId = ResolveFileId();
229 if (string.IsNullOrEmpty(resolvedId))
230 {
231 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for deletion", null, GPALObjectType.None);
232 return this;
233 }
234
235 var response = DriveRESTClient
236 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}")
237 .WithHttpMethod("DELETE")
238 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
239 .Execute();
240
241 if (response == null)
242 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to delete file [{resolvedId}]", null, GPALObjectType.None);
243
244 FileId = null;
245 return this;
246 }
247
248 public IAllowDriveActions RenameTo(string newName)
249 {
250 var resolvedId = ResolveFileId();
251 if (string.IsNullOrEmpty(resolvedId))
252 {
253 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for rename", null, GPALObjectType.None);
254 return this;
255 }
256
257 if (string.IsNullOrEmpty(newName))
258 {
259 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No new file name specified for rename", null, GPALObjectType.None);
260 return this;
261 }
262
263 try
264 {
265 var response = DriveRESTClient
266 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}")
267 .WithParameters(new { name = newName })
268 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
269 .WithHttpMethod("PATCH")
270 .Execute<DriveFileResponse>();
271
272 if (response == null)
273 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to rename file [{resolvedId}] to [{newName}]", null, GPALObjectType.None);
274 }
275 catch (Exception ex)
276 {
277 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to rename file [{resolvedId}]", null, GPALObjectType.None, ex);
278 }
279
280 return this;
281 }
282
283 public IAllowDriveActions MoveTo(string folderId)
284 {
285 var resolvedId = ResolveFileId();
286 if (string.IsNullOrEmpty(resolvedId))
287 {
288 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for move", null, GPALObjectType.None);
289 return this;
290 }
291
292 if (string.IsNullOrEmpty(folderId))
293 {
294 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No folder ID specified for move", null, GPALObjectType.None);
295 return this;
296 }
297
298 try
299 {
300 var getResponse = DriveRESTClient
301 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?fields=parents")
302 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
303 .Execute<DriveFileResponse>();
304
305 var currentParents = getResponse?.Parents ?? new List<string>();
306
307 var response = DriveRESTClient
308 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?addParents={folderId}&removeParents={string.Join(",", currentParents)}")
309 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
310 .WithHttpMethod("PATCH")
311 .Execute<DriveFileResponse>();
312
313 if (response == null)
314 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to move file [{resolvedId}] to folder [{folderId}]", null, GPALObjectType.None);
315 }
316 catch (Exception ex)
317 {
318 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to move file [{resolvedId}]", null, GPALObjectType.None, ex);
319 }
320
321 return this;
322 }
323
324 public IAllowDriveListOperations WithFileIds(out IGPALGrid<string> fileIds)
325 {
326 fileIds = GPAL.GridForType<string>();
327
328 if (string.IsNullOrEmpty(FileName))
329 {
330 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file name specified for getting IDs", null, GPALObjectType.None);
331 return this;
332 }
333
334 try
335 {
336 var query = $"name='{FileName}' and trashed=false";
337 var response = DriveRESTClient
338 .WithEndpoint($"{Config.FilesEndpoint}?q={Uri.EscapeDataString(query)}&fields=files({Config.QueryFields})")
339 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
340 .Execute<DriveFilesResponse>();
341
342 if (response?.Files != null)
343 foreach (var file in response.Files)
344 fileIds.AddRow(new List<string> { file.Id });
345 else
346 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to get IDs for file name [{FileName}]", null, GPALObjectType.None);
347 }
348 catch (Exception ex)
349 {
350 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to get IDs for file name [{FileName}]", null, GPALObjectType.None, ex);
351 }
352
353 return this;
354 }
355
356 public IAllowDriveListOperations ListFiles(out IGPALGrid<string> files)
357 {
358 files = GPAL.GridForType<string>();
359
360 try
361 {
362 var response = DriveRESTClient
363 .WithEndpoint($"{Config.FilesEndpoint}?fields=files({Config.QueryFields})")
364 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
365 .Execute<DriveFilesResponse>();
366
367 if (response?.Files != null)
368 {
369 foreach (var file in response.Files)
370 {
371 files.AddRow(new List<string> { file.Id, file.Name, file.MimeType, string.Join(",", file.Parents ?? new List<string>()) });
372 }
373 }
374 else
375 {
376 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Failed to list files", null, GPALObjectType.None);
377 }
378 }
379 catch (Exception ex)
380 {
381 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to list files", null, GPALObjectType.None, ex);
382 }
383
384 return this;
385 }
386
387 public IGoogleDrive ToGPALObject()
388 {
389 return this;
390 }
391 }
392
393 // Response classes for API calls
394 internal class DriveFilesResponse
395 {
396 public List<DriveFileResponse> Files { get; set; }
397 }
398
399 internal class DriveFileResponse
400 {
401 public string Id { get; set; }
402 public string Name { get; set; }
403 public string MimeType { get; set; }
404 public List<string> Parents { get; set; }
405 }
406}
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
List< string > Filenames
Get the list of filenames.
Definition GPALFile.cs:536
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowRESTEndpoint RESTClient
Instantiate a new fluent RESTClient.
Definition GPAL.cs:914
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2406