diff --git a/PhotosImporter.csproj b/PhotosImporter.csproj new file mode 100644 index 0000000..6a28044 --- /dev/null +++ b/PhotosImporter.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotosImporter.sln b/PhotosImporter.sln new file mode 100644 index 0000000..846e250 --- /dev/null +++ b/PhotosImporter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotosImporter", "PhotosImporter.csproj", "{0D052AAA-0255-4410-96C2-597DCC6A5236}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0D052AAA-0255-4410-96C2-597DCC6A5236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D052AAA-0255-4410-96C2-597DCC6A5236}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D052AAA-0255-4410-96C2-597DCC6A5236}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D052AAA-0255-4410-96C2-597DCC6A5236}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5D37C1A6-5006-443E-8B70-FE14194E14A8} + EndGlobalSection +EndGlobal diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a6e6387 --- /dev/null +++ b/Program.cs @@ -0,0 +1,303 @@ +using ManyConsole; +using MySql.Data.MySqlClient; +using Newtonsoft.Json; +using System.Data.Odbc; +using NExifTool; +using Nito.AsyncEx.Synchronous; +using Nito.AsyncEx; +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using System.Management.Automation; +using Microsoft.PowerShell; +using System.IO; +using Microsoft.PowerShell.Commands; +namespace PhotosImporter +{ + public class Program + { + public static int Main(string[] args) + { + var commands = GetCommands(); + + return ConsoleCommandDispatcher.DispatchCommand(commands, args, Console.Out); + } + + public static IEnumerable GetCommands() + { + return ConsoleCommandDispatcher.FindCommandsInSameAssemblyAs(typeof(Program)); + } + } + public class ImportCommand : ConsoleCommand + { + private const int Success = 0; + private const int Failure = 2; + + public string PhotosPath { get; set; } + public string PhotosTable { get; set; } + + private static bool HasNonASCIIChars(string str) + { + return (System.Text.Encoding.UTF8.GetByteCount(str) != str.Length); + } + + private static string[] GetBannedExtensions(string path) + { + string json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + + private static string[] GetVideoExtensions(string path) + { + string json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + + private static string Escape(string input) + { + return input.Replace("\"", "\\\"").Replace("'", "\\'"); + } + private static bool IsBadFilename(string filename) + { + bool prq1 = filename.Contains("'") || filename.Contains('"') || filename.Contains(',') || filename.Contains('`') || filename.Contains('’') || filename.Contains('—'); + bool prq2 = HasNonASCIIChars(filename); + + return (prq1 || prq2); + } + + private static void FixBadFilenameAndUpdateRecord(FileInfo file, string[] videoExtensions, string table) + { + char[] invalidChars = Path.GetInvalidFileNameChars(); + //invalidChars.Append('\''); + string newFileName = string.Join("_", Encoding.ASCII.GetString(ASCIIEncoding.Convert(Encoding.Unicode, Encoding.ASCII, Encoding.Unicode.GetBytes(file.Name))).Split(invalidChars)); + string newPath = Path.Combine(file.Directory.FullName,newFileName); + //string newFilename = filename.Replace("'", "");//.Replace('"', '').Replace(",", "").Replace("`", "").Replace("’", "").Replace("—", "-"); + //File.Move(table + filename, table + newFilename); + file.MoveTo(newPath); + UpdateRecord(new FileInfo(newPath), videoExtensions, table); + + } + + private static bool IsBannedExtension(string filename, string[] bannedExtensions) + { + string extension = Path.GetExtension(filename).ToLower(); + return bannedExtensions.Contains(extension); + } + + private static void UpdateRecord(FileInfo file, string[] videoExtensions, string table) + { + string name = Path.GetFileNameWithoutExtension(file.FullName); + string hash = GetFileHash(file); + string extension = file.Extension; + string tags = GetFileTags(file); + int isVideo = 0; + if (videoExtensions.Contains(extension)) + { + isVideo = 1; + } + double fileSizeMb = GetFileSizeMB(file); + + using (OdbcConnection connection = new OdbcConnection("DSN=photos")) + { + connection.Open(); + //string query = $"INSERT INTO {table} (name, hash, ext, tags, video, size) VALUES (@name, @hash, @ext, @tags, @video, @size)"; + string query = $"INSERT INTO {table} (name, hash, ext, tags, video, size) VALUES (\"{name}\", \"{Escape(hash)}\", \"{extension.Substring(1)}\", \'{tags}\', {isVideo.ToString()}, {fileSizeMb.ToString()})"; + //string query = $"INSERT INTO {table} (name, hash, ext, tags, video, size) VALUES (?, ?, ?, ?, ?, ?)"; + //Console.WriteLine(query); + OdbcCommand command = new(query, connection); + + //command.Parameters.AddWithValue("@name", name); + //command.Parameters.AddWithValue("@hash", hash); + //command.Parameters.AddWithValue("@ext", extension); + //command.Parameters.AddWithValue("@tags", tags); + //command.Parameters.AddWithValue("@video", isVideo); + //command.Parameters.AddWithValue("@size", fileSizeMb); + + Console.WriteLine(command.CommandText); + command.ExecuteNonQuery(); + } + } + + private static string GetFileHash(FileInfo file) + { + string res = ""; + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + if (file.Length < (500 * 1024 * 1024)) + { + using (var stream = File.OpenRead(file.FullName)) + { + var hash = md5.ComputeHash(stream); + res = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } else + { + PowerShell ps = PowerShell.Create(); + ps.AddCommand("Get-FileHash").AddParameter("Path", file.FullName).AddParameter("Algorithm", "MD5"); + dynamic results = ps.Invoke(); + foreach (var item in results) + { + //Type t = item.GetType(); + //if (t is FileHashInfo) + //{ + res = item.Hash.ToString().ToLowerInvariant(); + + } + } + return res; + } + } + + private static string GetFileTags(FileInfo file) + { + // Start the child process. + Process p = new(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "C:\\Users\\ZStrike\\.aftman\\bin\\exiftool.exe"; + p.StartInfo.Arguments = $"\"{(file.FullName)}\" -TagsList -json"; + // FileName = "C:\\Deps\\getopt.bat"; + p.Start(); + // Do not wait for the child process to exit before + // reading to the end of its redirected stream. + // p.WaitForExit(); + // Read the output stream first and then wait. + string output = p.StandardOutput.ReadToEnd(); + Console.WriteLine(output); + dynamic tmp = JsonConvert.DeserializeObject(output); + + p.WaitForExit(); + if (tmp[0].ContainsKey("TagsList")) { + return JsonConvert.SerializeObject(tmp[0]["TagsList"]); + } else + { + return "{}"; + } + + //Console.WriteLine("Line 100: Tags (none)"); + // This method is not implemented as it requires a third-party library to read EXIF tags. + // You can use a library like ExifLib or MetadataExtractor. + //ExifToolOptions opts = new ExifToolOptions { ExifToolPath = "C:\\Users\\ZStrike\\.aftman\\bin\\exiftool.exe" }; + + //var tagresult = new ExifTool(opts).GetTagsAsync(filePath).WaitAndUnwrapException(); + //Console.WriteLine(tagresult); + + } + + private static double GetFileSizeMB(FileInfo file) + { + + return (double)file.Length / (1024 * 1024); + } + + private static void WriteBadFileNotifications(List badFileNotifications) + { + using (StreamWriter writer = new StreamWriter("invalidfiles.txt")) + { + foreach (string notification in badFileNotifications) + { + writer.WriteLine(notification); + } + } + } + + private static void DeleteRecords(string table) + { + using (OdbcConnection connection = new OdbcConnection("DSN=photos")) + { + connection.Open(); + + string query = $"DELETE FROM {table}"; + OdbcCommand command = new OdbcCommand(query, connection); + + command.ExecuteNonQuery(); + Console.WriteLine("Deleted!"); + } + } + + private static void ReIndexRecords(string table) + { + using (OdbcConnection connection = new OdbcConnection("DSN=photos")) + { + connection.Open(); + + string query = $"ALTER TABLE {table} AUTO_INCREMENT = 1;"; + OdbcCommand command = new OdbcCommand(query, connection); + + command.ExecuteNonQuery(); + } + } + + public ImportCommand() { + // Register the actual command with a simple (optional) description. + IsCommand("Import", "Import Files"); + + // Add a longer description for the help on that specific command. + HasLongDescription("This can be used to quickly read a file's contents " + + "while optionally stripping out the ',' character."); + + // Required options/flags, append '=' to obtain the required value. + HasRequiredOption("p|path=", "Path", p => PhotosPath = p); + HasRequiredOption("t|table=", "Table", t => PhotosTable = t); + + // Optional options/flags, append ':' to obtain an optional value, or null if not specified. + //HasOption("s|strip:", "Strips ',' from the file before writing to output.", + // t => StripCommaCharacter = t == null ? true : Convert.ToBoolean(t)); + } + + public override int Run(string[] remainingArguments) + { + string sqlFilePath = Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "output.sql"); + System.IO.FileStream sqlFile = File.OpenWrite(sqlFilePath); // base class not allowed??? + + + string bannedExtensionsPath = "C:/Deps/bannedExtensions.json"; + string videoExtensionsPath = "C:/Deps/videoExtensions.json"; + + string[] bannedExtensions = GetBannedExtensions(bannedExtensionsPath); + string[] videoExtensions = GetVideoExtensions(videoExtensionsPath); + + List badFileNotifications = new List(); + Console.WriteLine("Deleting and re-indexing in 10 seconds..."); + // System.Threading.Thread.Sleep(10000); + + DeleteRecords(PhotosTable); + ReIndexRecords(PhotosTable); + foreach (string filename in Directory.GetFiles(PhotosPath)) + { + FileInfo file = new(filename); + if (!file.Name.StartsWith('.')) + { + Console.WriteLine($"{file.Name}"); + //Path.GetFileName(filename); + + if (IsBadFilename(file.Name) && !IsBannedExtension(file.Name, bannedExtensions)) + { + badFileNotifications.Add($"Bad Filename: {file}"); + Console.WriteLine($"Bad Filename: {file.Name}"); + FixBadFilenameAndUpdateRecord(file, videoExtensions, PhotosTable); + } + + if (!IsBannedExtension(file.Name, bannedExtensions)) + { + UpdateRecord(file, videoExtensions, PhotosTable); + } + Console.WriteLine("Processed: " + file.Name); + } + + WriteBadFileNotifications(badFileNotifications); + } + foreach (var item in badFileNotifications) + { + Console.WriteLine($"{item}"); + } + + + return Success; + + } + + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..fcce8cc --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "PhotosImporter": { + "commandName": "Project", + "commandLineArgs": "Import -t test -p C:\\Users\\ZStrike\\Downloads\\DPRK\\" + } + } +} \ No newline at end of file