-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProgram.cs
395 lines (360 loc) · 17.8 KB
/
Program.cs
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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using coynesolutions.treeupload.SmugMug;
using System.Net;
namespace coynesolutions.treeupload
{
class Program
{
private static string logFile;
private static HashSet<string> FilesToSkip = new HashSet<string>((ConfigurationManager.AppSettings["FilesToSkip"] ?? "").Split(new[] { ' ', ';', ',' }, StringSplitOptions.RemoveEmptyEntries), StringComparer.InvariantCultureIgnoreCase);
static void Main(string[] args)
{
logFile = "treeupload.log.txt";
Trace.Listeners.Add(new ConsoleTraceListener());
if (ConfigurationManager.AppSettings["Log"] == "true")
{
Trace.Listeners.Add(new TextWriterTraceListener(logFile));
}
//SmugMugTestSort();
Upload<SmugMugUploader>(args?.ElementAtOrDefault(0));
Console.WriteLine("All done. Press a key to exit...");
Console.ReadKey();
}
private static int dupeIndent;
private static void SmugMugDupeFoldersTest(IFolder folder = null)
{
folder = folder ?? new SmugMugUploader().RootFolder;
Trace.WriteLine(new string('\t', dupeIndent++) + folder.Name);
var subFolders = folder.SubFolders.ToArray();
foreach (var dupe in subFolders.GroupBy(f => f.Name, f => f).Where(g => g.Count() > 1).Select(g => new {Name = g.Key, Count = g.Count()}))
{
Trace.WriteLine(new string('\t', dupeIndent) + dupe.Name + ": " + dupe.Count);
}
foreach (var subFolder in subFolders)
{
SmugMugDupeFoldersTest(subFolder);
}
dupeIndent--;
}
private static void KeywordTest()
{
var uploader = new SmugMugUploader();
uploader.RemoveBadKeywords();
}
private static void SmugMugTestSort()
{
var u = new SmugMugUploader();
//foreach (var folder in u.RootFolder.SubFolders.Single(f => f.Name == "2016").SubFolders.Where(f => f.Name.StartsWith("2016\\20160312")))
foreach (var folder in u.RootFolder.SubFolders.Single(f => f.Name == "2017").SubFolders)
{
folder.Sort();
}
}
private static void Upload<T>(string subdir = null) where T : IUploader, new()
{
IUploader uploader = new T();
uploader.UploadProgress += (sender, args) => Console.Write("\r" + args.FractionComplete.ToString("P"));
var rootImagesFolder = ConfigurationManager.AppSettings["ImageFolder"]; // move that to uploader?
var folderToUpload = Path.Combine(rootImagesFolder, subdir ?? ""); // upload only from this subdirectory of the rootImagesFolder. The arg default is null rather than "", coalesced here, so it'll work if null is passed in.
if (!folderToUpload.StartsWith(rootImagesFolder, StringComparison.InvariantCultureIgnoreCase))
{
throw new Exception("Folder to upload must be under ImageFolder!");
}
if (string.IsNullOrWhiteSpace(rootImagesFolder) || !Directory.Exists(rootImagesFolder))
{
throw new Exception("Configured images folder does not exist: " + rootImagesFolder);
}
string lastDirectory = null;
var anyImagesUploaded = false;
IFolder folder = null;
Dictionary<string, IImage> albumImages = null;
// Specify actions to be run when we move to a new directory, plus when we are all finished (for the last directory).
// I want "access to modified closure" -- that is, I want the latest values of all the variables accessed within this Action-- so I will ignore resharper's warning.
// ReSharper disable AccessToModifiedClosure
Action postDirectoryCleanup = () =>
{
// clean up from last directory
if (albumImages != null && albumImages.Count > 0)
{
Trace.WriteLine("WARNING: Extra images in album " + folder.Name);
foreach (var extra in albumImages.Keys)
{
Trace.WriteLine("\t" + extra);
}
albumImages.Clear(); // not really necessary since it's reassigned below, but no point in keeping this around
}
if (folder != null)
{
Trace.WriteLine("anyImagesUploaded? " + anyImagesUploaded);
try
{
folder.Sort(anyImagesUploaded);
}
catch(WebException e)
{
Console.WriteLine("Sort failed! " + e.Message);
}
anyImagesUploaded = false;
}
};
// ReSharper restore AccessToModifiedClosure
foreach (var file in Directory.EnumerateFiles(folderToUpload, "*", SearchOption.AllDirectories))
{
if (!uploader.SupportedExtensions.Contains(Path.GetExtension(file).ToLowerInvariant()))
{
continue;
}
if(ShouldSkipFile(file))
{
continue;
}
Trace.WriteLine(file);
var directory = Path.GetDirectoryName(file);
if(!directory.Equals(lastDirectory, StringComparison.InvariantCultureIgnoreCase))
{
postDirectoryCleanup();
if (!directory.StartsWith(rootImagesFolder, StringComparison.InvariantCultureIgnoreCase))
{
throw new Exception("Directory isn't in images folder?");
}
var relativeDirectory = directory.Substring(rootImagesFolder.Length).Trim('\\');
var parts = relativeDirectory.Split('\\');
TransformPath(uploader, ref parts);
var currentFolder = uploader.RootFolder;
for (var i = 0; i < parts.Length; i++)
{
IFolder nextFolder = null;
if (relativeDirectory != "Other")
{
nextFolder = currentFolder.SubFolders.SingleOrDefault(f => f.Name == relativeDirectory);
}
else
{
Console.WriteLine("Not putting stuff in Other...");
}
if (nextFolder == null)
{
nextFolder = currentFolder.SubFolders.SingleOrDefault(f => f.Name == parts[i] || f.Name == string.Join("\\", parts.Take(i + 1)));
}
if (nextFolder == null)
{
// create new one inside folder?
var newFolderName = relativeDirectory;
var folderContainsImages = true;
if (i == 1 && parts[0] == OtherPeopleSmugMugCategory && parts.Length > 2)
{
newFolderName = parts[1];
folderContainsImages = false;
}
else if (i == 2 && parts.Length == 3 && parts[0] == "Other" && parts[1] == "Slides")
{
newFolderName = parts[2];
}
nextFolder = currentFolder.CreateSubFolder(newFolderName, folderContainsImages); // TODO: this is totally specific to my config... normally you might want to have node folders with just parts[i] for their names, and leaf albums with either parts[i] or relativeDirectory for their names
// TODO: maybe have a few configurable options...
// my smugmug style, where it's always folder/category "tldname", album "tldname\dirname[\dirname]" (except one level deeper under "Other People"!)
// albums for directories containing images, otherwise folders
// folders for all directories; extra albums (maybe with the full relativePath as their name) inside of those containing images
}
currentFolder = nextFolder;
Trace.WriteLine(string.Format("Folder {0} has {1} subfolders", currentFolder.Name, currentFolder.SubFolders.Count()));
if (currentFolder.Name == relativeDirectory)
{
break;
}
}
folder = currentFolder;
Trace.WriteLine(directory + "\t->\t" + folder.Name);
// do stuff... check and see if the album exists and so forth
lastDirectory = directory;
// any duplicate filenames in this album?
var imagesByFilename = folder.Images.GroupBy(i => i.FileName).ToArray();
var duplicateFilenames = imagesByFilename.Where(g => g.Count() > 1).ToArray();
if (duplicateFilenames.Any())
{
Trace.WriteLine("WARNING: duplicate filenames in album " + folder.Name);
foreach (var dupe in duplicateFilenames.Select(g => g.Key))
{
Trace.WriteLine("\t" + dupe);
}
}
// make a list of filenames. we'll take out each one as we see it, and any left over are extra, in the album but not on disk.
albumImages = imagesByFilename.ToDictionary(g => g.Key, g => g.First(), StringComparer.InvariantCultureIgnoreCase);
}
IImage matchingImage;
if(TryGetMatchingImage(albumImages, Path.GetFileName(file), out matchingImage))
{
albumImages.Remove(matchingImage.FileName);
//// see if it matches the image on disk
//var fileInfo = new FileInfo(file);
//if (fileInfo.Length != matchingImage.Size)
//{
// Console.WriteLine("WARNING: Size doesn't match for " + file + ": " + fileInfo.Length + " <> " + matchingImage.Size);
//}
////if (matchingImage.Date.HasValue && fileInfo.LastWriteTime != matchingImage.Date) // checking the date causes another http request, for image metadata
////{
//// Debug.WriteLine("Date doesn't match!");
////}
//var md5 = GetMd5(file);
//if (md5 != matchingImage.MD5)
//{
// Console.WriteLine("WARNING: MD5 doesn't match for " + file + ": " + md5 + " <> " + matchingImage.MD5);
//}
}
else
{
Trace.WriteLine(string.Format("Uploading {0} to {1}", file, folder.Name));
if (UploadWithRetry(folder, file))
{
anyImagesUploaded = true;
}
}
}
postDirectoryCleanup();
}
private static bool TryGetMatchingImage(Dictionary<string, IImage> albumImages, string file, out IImage matchingImage)
{
if(albumImages.TryGetValue(Path.GetFileName(file), out matchingImage))
{
return true;
}
if(file.Any(c => c > 256))
{
var fixedCharsFile = new string(file.Select(c => c > 256 ? (char)(c - 256) : c).ToArray());
if(albumImages.TryGetValue(Path.GetFileName(fixedCharsFile), out matchingImage))
{
Trace.WriteLine("Compensated for dumb characters in filename being stripped by SmugMug");
return true;
}
}
if(Path.GetExtension(file).ToLowerInvariant() == ".heic")
{
var heicFilename = Path.GetFileNameWithoutExtension(file) + "_heic.JPG";
if(albumImages.TryGetValue(heicFilename, out matchingImage))
{
return true;
}
var jpgFilename = Path.GetFileNameWithoutExtension(file) + ".JPG";
if (albumImages.TryGetValue(jpgFilename, out matchingImage))
{
return true;
}
}
var mangledName = $"{Path.GetFileNameWithoutExtension(file)}_{Path.GetExtension(file).TrimStart('.')}{Path.GetExtension(file)}";
if (albumImages.TryGetValue(Path.GetFileName(mangledName), out matchingImage))
{
return true;
}
return false;
}
private static bool UploadWithRetry(IFolder folder, string file)
{
const int attempts = 3;
var attempt = 1;
while(true)
{
try
{
return folder.Upload(file);
}
catch (Exception e)
{
Trace.WriteLine("Upload failed: " + e.Message);
var webEx = e as WebException;
if(webEx != null)
{
var response = webEx.Response as HttpWebResponse;
if(response != null)
{
if(response.StatusCode == HttpStatusCode.Unauthorized)
{
Trace.WriteLine("Got a 401. It probably worked... returning true!");
return true;
}
}
Trace.WriteLine("WebException");
}
if (attempt < attempts)
{
attempt++;
Trace.WriteLine("Retrying...");
}
else
{
throw;
}
}
}
}
private const string OtherPeopleSmugMugCategory = "Other People";
private static void TransformPath(IUploader uploader, ref string[] directories)
{
// TODO: make this transform logic customizable somehow
if (directories.Length == 0)
{
return;
}
var firstPart = directories[0];
if (directories.Length == 1 && firstPart == "Other")
{
var temp2 = new List<string>(directories);
temp2.Insert(0, "Test Junk");
directories = temp2.ToArray();
return;
}
if (directories[0] == "MotoPhotoCD")
{
var temp2 = new List<string>(directories);
temp2.Insert(0, "2003");
directories = temp2.ToArray();
return;
}
if(directories.Length == 3 && directories[0] == "Other People's Cameras" && directories[1] == "Mom" && directories[2] == "20080901")
{
directories = new[] { "2008", @"Other People's Cameras\Mom\20080901" };
}
const string otherPeopleDiskDirectory = "Other People's Cameras";
if (firstPart == otherPeopleDiskDirectory)
{
directories[0] = OtherPeopleSmugMugCategory; // put them in there instead
}
else if (uploader.RootFolder.SubFolders.All(f => f.Name != firstPart))
{
// this file's top-level folder doesn't exist on smugmug.
var pathString = string.Join("\\", directories);
if (uploader.RootFolder.SubFolders.Single(f => f.Name == OtherPeopleSmugMugCategory).SubFolders.Any(f => f.Name == firstPart || f.Name == pathString))
{
var temp2 = new List<string>(directories);
temp2.Insert(0, OtherPeopleSmugMugCategory);
directories = temp2.ToArray();
return;
}
int i;
if (int.TryParse(firstPart, out i) && 1999 <= i && i <= DateTime.Now.Year)
{
// year directory... it's OK for it to create it
return;
}
const string DefaultFolderName = "Other"; // move this to SmugMugUploader? Config setting
if (uploader.RootFolder.SubFolders.All(f => f.Name != DefaultFolderName))
{
throw new Exception("Default folder name doesn't exist!");
// Maybe I should just create it?
}
// Stick it under the default top-level folder
var temp = new List<string>(directories);
temp.Insert(0, DefaultFolderName);
directories = temp.ToArray();
}
}
private static bool ShouldSkipFile(string filename)
{
return FilesToSkip.Contains(filename);
}
}
}