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 [Settings(Description = "The Google account to use Drive as")]
43 IAllowDriveFileSelection WithCredentials(ICredentials credentials);
44 }
45
46 public interface IAllowDriveFileSelection : IAllowToGPALObject<IGoogleDrive>
47 {
48 [Settings(Description = "The file on this machine to upload")]
49 IAllowLocalFileOperations WithLocalFile(GPALFile file);
50 [Settings(Description = "The Drive file to work on, by id")]
51 IAllowRemoteFileOperations WithFileId(string fileId);
52 [Settings(Description = "The Drive file to work on, by name")]
53 IAllowRemoteFileOperations WithFileName(string name);
54 }
55
57 {
58 [Action(Description = "Upload the file to the Drive folder")]
59 IAllowDriveActions UploadTo(string fileName);
60 }
61
63 {
64 [Action(Description = "Download the Drive file to this machine")]
65 IAllowDriveActions SaveTo(GPALFile destination);
66 [File(Description = "Delete the Drive file")]
67 IAllowDriveActions DeleteFile();
68 [Action(Description = "Move the Drive file to another folder")]
69 IAllowDriveActions MoveTo(string folderId);
70 [File(Description = "Rename the Drive file")]
71 IAllowDriveActions RenameTo(string newName);
72 [Settings(Description = "The ids of every Drive file with the chosen name")]
73 IAllowDriveListOperations WithFileIds(out IGPALGrid<string> fileIds);
74 [Action(Description = "List the files in Drive")]
75 IAllowDriveListOperations ListFiles(out IGPALGrid<string> files);
76 }
77
79 {
80 }
81
85
89
90 // Main GoogleDrive class
91 public class GoogleDrive : IGoogleDrive
92 {
93 internal GoogleDriveConfig Config { get; private set; }
94 internal IRESTClient DriveRESTClient { get; private set; }
95 internal ICredentials Credentials { get; private set; }
96 internal GPALFile LocalFile { get; private set; }
97 internal string FileId { get; private set; }
98 internal string FileName { get; private set; }
99
100 internal GoogleDrive()
101 {
102 Config = new GoogleDriveConfig();
103 DriveRESTClient = GPAL.RESTClient.WithAPIBase(Config.DriveApiBase).ToGPALObject();
104 }
105
106 public IAllowDriveFileSelection WithCredentials(ICredentials credentials)
107 {
108 Credentials = credentials;
109 return this;
110 }
111
112 public IAllowLocalFileOperations WithLocalFile(GPALFile file)
113 {
114 LocalFile = file;
115 return this;
116 }
117
118 public IAllowRemoteFileOperations WithFileId(string fileId)
119 {
120 FileId = fileId;
121 return this;
122 }
123
124 public IAllowRemoteFileOperations WithFileName(string name)
125 {
126 FileName = name;
127 return this;
128 }
129
130 private string GetToken()
131 {
132 Credentials.FetchAccessToken(out string token);
133 return token;
134 }
135
136 private string ResolveFileId()
137 {
138 if (false == string.IsNullOrEmpty(FileId))
139 return FileId;
140
141 if (string.IsNullOrEmpty(FileName))
142 return null;
143
144 var query = $"name='{FileName}' and trashed=false";
145 var response = DriveRESTClient
146 .WithEndpoint($"{Config.FilesEndpoint}?q={Uri.EscapeDataString(query)}&fields=files(id)")
147 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
148 .Execute<DriveFilesResponse>();
149
150 return response?.Files?.FirstOrDefault()?.Id;
151 }
152
153 public IAllowDriveActions UploadTo(string fileName)
154 {
155 if (LocalFile == null || LocalFile.Filenames.Count == 0)
156 {
157 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No local file specified for upload", null, GPALObjectType.None);
158 return this;
159 }
160
161 string filePath = LocalFile.Filenames.First();
162
163 FileName = fileName;
164 string existingId = ResolveFileId();
165 if (existingId != null)
166 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"A file named [{fileName}] already exists in Drive (id: [{existingId}]). Drive will create a duplicate.", null, GPALObjectType.None);
167 FileName = null;
168
169 try
170 {
171 var content = new MultipartFormDataContent();
172 content.Add(new StringContent($"{{ \"name\": \"{fileName}\" }}"), "metadata", "application/json");
173 content.Add(new ByteArrayContent(File.ReadAllBytes(filePath)), "file", fileName);
174
175 var response = DriveRESTClient
176 .WithEndpoint($"{Config.UploadEndpoint}{Config.UploadType}")
177 .WithParameters(content)
178 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
179 .Execute<DriveFileResponse>();
180
181 if (response == null)
182 {
183 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to upload file [{filePath}]", null, GPALObjectType.None);
184 }
185 else
186 {
187 FileId = response.Id;
188 }
189 }
190 catch (Exception ex)
191 {
192 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to upload file [{filePath}]", null, GPALObjectType.None, ex);
193 }
194
195 return this;
196 }
197
198 public IAllowDriveActions SaveTo(GPALFile destination)
199 {
200 var resolvedId = ResolveFileId();
201 if (string.IsNullOrEmpty(resolvedId))
202 {
203 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for download", null, GPALObjectType.None);
204 return this;
205 }
206
207 if (destination == null || destination.Filenames.Count == 0)
208 {
209 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No destination file specified for download", null, GPALObjectType.None);
210 return this;
211 }
212
213 string destinationPath = destination.Filenames.First();
214 try
215 {
216 var bytes = DriveRESTClient
217 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?alt=media")
218 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
219 .Execute<byte[]>();
220
221 if (bytes != null)
222 {
223 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{bytes.Length}] bytes of drive file [{resolvedId}] to [{destinationPath}].", null, GPALObjectType.None);
224 File.WriteAllBytes(destinationPath, bytes);
225 }
226 else
227 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to download file [{resolvedId}]", null, GPALObjectType.None);
228 }
229 catch (Exception ex)
230 {
231 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to download file [{resolvedId}]", null, GPALObjectType.None, ex);
232 }
233
234 return this;
235 }
236
237 public IAllowDriveActions DeleteFile()
238 {
239 var resolvedId = ResolveFileId();
240 if (string.IsNullOrEmpty(resolvedId))
241 {
242 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for deletion", null, GPALObjectType.None);
243 return this;
244 }
245
246 var response = DriveRESTClient
247 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}")
248 .WithHttpMethod("DELETE")
249 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
250 .Execute();
251
252 if (response == null)
253 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to delete file [{resolvedId}]", null, GPALObjectType.None);
254
255 FileId = null;
256 return this;
257 }
258
259 public IAllowDriveActions RenameTo(string newName)
260 {
261 var resolvedId = ResolveFileId();
262 if (string.IsNullOrEmpty(resolvedId))
263 {
264 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for rename", null, GPALObjectType.None);
265 return this;
266 }
267
268 if (string.IsNullOrEmpty(newName))
269 {
270 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No new file name specified for rename", null, GPALObjectType.None);
271 return this;
272 }
273
274 try
275 {
276 var response = DriveRESTClient
277 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}")
278 .WithParameters(new { name = newName })
279 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
280 .WithHttpMethod("PATCH")
281 .Execute<DriveFileResponse>();
282
283 if (response == null)
284 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to rename file [{resolvedId}] to [{newName}]", null, GPALObjectType.None);
285 }
286 catch (Exception ex)
287 {
288 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to rename file [{resolvedId}]", null, GPALObjectType.None, ex);
289 }
290
291 return this;
292 }
293
294 public IAllowDriveActions MoveTo(string folderId)
295 {
296 var resolvedId = ResolveFileId();
297 if (string.IsNullOrEmpty(resolvedId))
298 {
299 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file specified for move", null, GPALObjectType.None);
300 return this;
301 }
302
303 if (string.IsNullOrEmpty(folderId))
304 {
305 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No folder ID specified for move", null, GPALObjectType.None);
306 return this;
307 }
308
309 try
310 {
311 var getResponse = DriveRESTClient
312 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?fields=parents")
313 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
314 .Execute<DriveFileResponse>();
315
316 var currentParents = getResponse?.Parents ?? new List<string>();
317
318 var response = DriveRESTClient
319 .WithEndpoint($"{Config.FilesEndpoint}/{resolvedId}?addParents={folderId}&removeParents={string.Join(",", currentParents)}")
320 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
321 .WithHttpMethod("PATCH")
322 .Execute<DriveFileResponse>();
323
324 if (response == null)
325 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to move file [{resolvedId}] to folder [{folderId}]", null, GPALObjectType.None);
326 }
327 catch (Exception ex)
328 {
329 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to move file [{resolvedId}]", null, GPALObjectType.None, ex);
330 }
331
332 return this;
333 }
334
335 public IAllowDriveListOperations WithFileIds(out IGPALGrid<string> fileIds)
336 {
337 fileIds = GPAL.GridForType<string>();
338
339 if (string.IsNullOrEmpty(FileName))
340 {
341 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No file name specified for getting IDs", null, GPALObjectType.None);
342 return this;
343 }
344
345 try
346 {
347 var query = $"name='{FileName}' and trashed=false";
348 var response = DriveRESTClient
349 .WithEndpoint($"{Config.FilesEndpoint}?q={Uri.EscapeDataString(query)}&fields=files({Config.QueryFields})")
350 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
351 .Execute<DriveFilesResponse>();
352
353 if (response?.Files != null)
354 foreach (var file in response.Files)
355 fileIds.AddRow(new List<string> { file.Id });
356 else
357 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to get IDs for file name [{FileName}]", null, GPALObjectType.None);
358 }
359 catch (Exception ex)
360 {
361 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to get IDs for file name [{FileName}]", null, GPALObjectType.None, ex);
362 }
363
364 return this;
365 }
366
367 public IAllowDriveListOperations ListFiles(out IGPALGrid<string> files)
368 {
369 files = GPAL.GridForType<string>();
370
371 try
372 {
373 var response = DriveRESTClient
374 .WithEndpoint($"{Config.FilesEndpoint}?fields=files({Config.QueryFields})")
375 .WithHeader(Config.AuthorizationHeader, $"{Config.Bearer} {GetToken()}")
376 .Execute<DriveFilesResponse>();
377
378 if (response?.Files != null)
379 {
380 foreach (var file in response.Files)
381 {
382 files.AddRow(new List<string> { file.Id, file.Name, file.MimeType, string.Join(",", file.Parents ?? new List<string>()) });
383 }
384 }
385 else
386 {
387 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Failed to list files", null, GPALObjectType.None);
388 }
389 }
390 catch (Exception ex)
391 {
392 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to list files", null, GPALObjectType.None, ex);
393 }
394
395 return this;
396 }
397
398 public IGoogleDrive ToGPALObject()
399 {
400 return this;
401 }
402 }
403
404 // Response classes for API calls
405 internal class DriveFilesResponse
406 {
407 public List<DriveFileResponse> Files { get; set; }
408 }
409
410 internal class DriveFileResponse
411 {
412 public string Id { get; set; }
413 public string Name { get; set; }
414 public string MimeType { get; set; }
415 public List<string> Parents { get; set; }
416 }
417}
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:540
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:950
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:2568