for Push.
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
using Npgsql;
|
||||
using DocumentFormat.OpenXml.Drawing;
|
||||
using DocumentFormat.OpenXml.Office.Word;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using KssSmaPlaLib.Commons;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.Entity;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using static KssSmaPlaLib.IO.Database.DbUxer;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace KssSmaPlaLib.IO.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// DBアクセスアシスタント
|
||||
/// </summary>
|
||||
public class DbUxer : IDisposable
|
||||
{
|
||||
public enum DbProvider
|
||||
{
|
||||
PostgreSQL,
|
||||
SQLServer,
|
||||
MySQL,
|
||||
Oracle,
|
||||
SQLite,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// プロバイダ名ディクショナリ
|
||||
/// </summary>
|
||||
private static readonly Dictionary<DbProvider, string> _providerNameDic = new Dictionary<DbProvider, string>()
|
||||
{
|
||||
{ DbProvider.PostgreSQL, "Npgsql" },
|
||||
{ DbProvider.SQLServer, "System.Data.SqlClient" },
|
||||
{ DbProvider.MySQL, "MySql.Data.MySqlClient" },
|
||||
{ DbProvider.Oracle, "Oracle.ManagedDataAccess.Client" },
|
||||
{ DbProvider.SQLite, "System.Data.SQLite" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// プロバイダ名(文字列)からenum値に変更
|
||||
/// </summary>
|
||||
/// <param name="dbProviderName">プロバイダ名(文字列)</param>
|
||||
/// <returns>enum値(該当なし時はnull)</returns>
|
||||
public static DbProvider? GetDbProviderEnumFromString(string dbProviderName = "PostgreSQL")
|
||||
{
|
||||
switch(dbProviderName.ToLower().Replace(" ",""))
|
||||
{
|
||||
case "postgresql":
|
||||
case "ポスグレ":
|
||||
return DbProvider.PostgreSQL;
|
||||
case "sqlserver":
|
||||
return DbProvider.SQLServer;
|
||||
case "mysql":
|
||||
return DbProvider.MySQL;
|
||||
case "oracle":
|
||||
case "oracledatabase":
|
||||
return DbProvider.Oracle;
|
||||
case "sqlite":
|
||||
case "sqlite3":
|
||||
return DbProvider.SQLite;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// コミット済みフラグ
|
||||
/// </summary>
|
||||
private bool _isCommitted = false;
|
||||
|
||||
/// <summary>
|
||||
/// SQLディクショナリ
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _sqlDic = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// 指定フォルダからSQLファイルを読み込む(ディクショナリ保存)
|
||||
/// </summary>
|
||||
/// <param name="folderPath">フォルダパス</param>
|
||||
/// <returns>成否</returns>
|
||||
public static bool TryLoadSqlFromText(string folderPath, out string resultMessage)
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
var fileCount = 0;
|
||||
List<string> files = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(folderPath))
|
||||
{
|
||||
resultMessage = "フォルダパスが指定されていません。";
|
||||
return false;
|
||||
}
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
resultMessage = $"フォルダが存在しません。[フォルダパス:{folderPath}]";
|
||||
return false;
|
||||
}
|
||||
_sqlDic.Clear();
|
||||
foreach (var file in Directory.GetFiles(folderPath, "*.sql.txt"))
|
||||
{
|
||||
_sqlDic.Add(Path.GetFileNameWithoutExtension(file).ToLower().Replace(".sql", ""), System.IO.File.ReadAllText(file));
|
||||
files.Add(Path.GetFileNameWithoutExtension(file).Replace(".sql", ""));
|
||||
fileCount++;
|
||||
}
|
||||
resultMessage = $"SQLファイルをロードしました。[ファイル数:{fileCount},ファイル:{string.Join(",", files)}]";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"SQLファイル読み込み時にエラーが発生しました。[エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TestConnectByDic(Dictionary<string,string> iniDic, out string resultMessage)
|
||||
{
|
||||
const string KEY_DB_PROVIDER_NAME = "DbProviderName";
|
||||
const string KEY_DB_CONNECTION_STRING = "DbConnectionString";
|
||||
|
||||
var dbProviderName = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty);
|
||||
var connectionString = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty);
|
||||
return TestConnect(dbProviderName, connectionString, out resultMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// テスト接続
|
||||
/// </summary>
|
||||
/// <returns>成否</returns>
|
||||
public static bool TestConnect(string dbPrividerName, string dbConnectionString, out string resultMessage)
|
||||
{
|
||||
// パラメータチェック&DBプロバイダ取得
|
||||
if (!TryConnectParameter(dbPrividerName, dbConnectionString, out DbProvider? _, out resultMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryCreate(dbPrividerName, dbConnectionString, out var dbUxer, out resultMessage, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
dbUxer.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DB接続
|
||||
/// </summary>
|
||||
private DbConnection _con = null;
|
||||
|
||||
/// <summary>
|
||||
/// トランザクション
|
||||
/// </summary>
|
||||
private DbTransaction _tran = null;
|
||||
|
||||
public static bool TryCreateByDic(Dictionary<string, string> iniDic, out DbUxer dbUxer, out string resultMessage, bool useTransaction = false)
|
||||
{
|
||||
const string KEY_DB_PROVIDER_NAME = "DbProviderName";
|
||||
const string KEY_DB_CONNECTION_STRING = "DbConnectionString";
|
||||
|
||||
var dbProviderName = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty);
|
||||
var connectionString = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty);
|
||||
|
||||
return TryCreate(dbProviderName, connectionString, out dbUxer, out resultMessage, useTransaction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成挑戦
|
||||
/// </summary>
|
||||
/// <param name="dbPrividerName"></param>
|
||||
/// <param name="dbProvider"></param>
|
||||
/// <param name="dbConnectionString"></param>
|
||||
/// <param name="dbUxer"></param>
|
||||
/// <param name="resultMessage"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryCreate(string dbPrividerName, string dbConnectionString, out DbUxer dbUxer, out string resultMessage, bool useTransaction = false)
|
||||
{
|
||||
dbUxer = null;
|
||||
|
||||
// パラメータチェック&DBプロバイダ取得
|
||||
if (!TryConnectParameter(dbPrividerName, dbConnectionString, out DbProvider? dbp, out resultMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DbProvider dbpp = (DbProvider)dbp;
|
||||
|
||||
try
|
||||
{
|
||||
// 生成
|
||||
dbUxer = new DbUxer(dbpp, dbConnectionString, useTransaction);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 接続NG回答
|
||||
resultMessage = $"DB接続に失敗しました。[エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryConnectParameter(string dbPrividerName, string dbConnectionString, out DbProvider? dbp, out string resultMessage)
|
||||
{
|
||||
dbp = DbProvider.PostgreSQL;
|
||||
resultMessage = string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(dbPrividerName))
|
||||
{
|
||||
// DBプロバイダ指定なしの場合、デフォルトはポスグレとする
|
||||
// Infoメッセージ
|
||||
resultMessage = "DBプロバイダ名が未指定のため、デフォルトのPostgreSQLを使用します。";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(dbConnectionString))
|
||||
{
|
||||
resultMessage = "接続文字列が設定されていません。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dbPrividerName))
|
||||
{
|
||||
dbp = GetDbProviderEnumFromString(dbPrividerName);
|
||||
}
|
||||
|
||||
if (dbp == null)
|
||||
{
|
||||
resultMessage = $"DBプロバイダ名が不正です。[DBプロバイダ名:{dbPrividerName}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// コンストラクタ(DB接続)
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">DBプロバイダ(enum値)</param>
|
||||
/// <param name="connectionString">接続文字列</param>
|
||||
/// <param name="useTransaction">トランザクション有無</param>
|
||||
private DbUxer(DbProvider dbProvider, string connectionString, bool useTransaction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// DBプロバイダファクトリー作成
|
||||
DbProviderFactory factory;
|
||||
if (dbProvider == DbProvider.PostgreSQL)
|
||||
{
|
||||
// ポスグレの場合、Npgsqlのインスタンをを直接取得
|
||||
factory = Npgsql.NpgsqlFactory.Instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
factory = DbProviderFactories.GetFactory(_providerNameDic[dbProvider]);
|
||||
}
|
||||
|
||||
// DB接続作成
|
||||
_con = factory.CreateConnection();
|
||||
|
||||
// 接続文字列設定
|
||||
_con.ConnectionString = connectionString;
|
||||
|
||||
// 接続
|
||||
_con.Open();
|
||||
|
||||
// トランザクション
|
||||
if (useTransaction) _tran = _con.BeginTransaction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerUxer.Error($"DB接続に失敗しました。[プロバイダ:{dbProvider},接続文字列:{connectionString},エラー内容:{ex.Message}]");
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ExecuteNonQuery(string sqlOrName, Dictionary<string, object> parameters, out int resultCount, out string resultMessage)
|
||||
{
|
||||
resultCount = 0;
|
||||
resultMessage = string.Empty;
|
||||
|
||||
if (_con == null || _con.State != ConnectionState.Open)
|
||||
{
|
||||
resultMessage = "DBと接続されていません。";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var cmd = _con.CreateCommand())
|
||||
{
|
||||
// コパンドプロパティを注入
|
||||
if (!TryInjectSoulToCmd(cmd, sqlOrName, parameters, out var messageForMe))
|
||||
{
|
||||
resultMessage = messageForMe;
|
||||
return false;
|
||||
}
|
||||
|
||||
resultCount = cmd.ExecuteNonQuery();
|
||||
LoggerUxer.Info($"[Rec Count:{resultCount}]");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"SQL実行でエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool BeginTextImport(
|
||||
string sqlOrName,
|
||||
string csvFilePath,
|
||||
out int resultCount,
|
||||
out string resultMessage)
|
||||
{
|
||||
resultCount = 0;
|
||||
resultMessage = string.Empty;
|
||||
|
||||
if (_con == null || _con.State != ConnectionState.Open)
|
||||
{
|
||||
resultMessage = "DBと接続されていません。";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// SQLの取得
|
||||
sqlOrName = sqlOrName.ToLower();
|
||||
var sql = _sqlDic.ContainsKey(sqlOrName) ? _sqlDic[sqlOrName] : sqlOrName;
|
||||
|
||||
// CSV有無
|
||||
if (!System.IO.File.Exists(csvFilePath))
|
||||
{
|
||||
resultMessage = $"CSVファイルが存在しません。[ファイルパス:{csvFilePath}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
// バルクインサート用のメソッドはポスグレの接続しか持たない
|
||||
var pgConn = (NpgsqlConnection)_con;
|
||||
|
||||
var sqls = sql.Split(new[] { "-- SPLIT --" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (sqls.Length != 3)
|
||||
{
|
||||
resultMessage = $"バルク挿入SQLで分割数が正しくありません。[SQLファイル名:{sqlOrName},分割数:{sqls.Length}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1個目のSQL(一時テーブル作成)
|
||||
if (!ExecuteNonQuery(sqls[0], new Dictionary<string, object>(), out var _, out var messageForMe))
|
||||
{
|
||||
resultMessage = $"バルク挿入SQL(1)が失敗しました。[詳細:{messageForMe}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2個目のSQL(一時テーブルにバルク挿入)
|
||||
using (var writer = pgConn.BeginTextImport(sqls[1]))
|
||||
{
|
||||
using (var reader = new StreamReader(csvFilePath))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
writer.WriteLine(line);
|
||||
//resultCount++; // ここで「1件、2件…」と数える!♨️
|
||||
}
|
||||
}
|
||||
} // これで完了!1000件なら「シュンッ!」で終わります
|
||||
|
||||
// 3個目のSQL(一時テーブルから対象テーブルへコピー)
|
||||
if (!ExecuteNonQuery(sqls[2], new Dictionary<string, object>(), out resultCount, out messageForMe))
|
||||
{
|
||||
resultMessage = $"バルク挿入SQL(2)が失敗しました。[詳細:{messageForMe}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
LoggerUxer.Info($"[Rec Count:{resultCount}]");
|
||||
return true;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"SQL実行でエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ExecuteQuery(string sqlOrName, Dictionary<string, object> parameters, out List<Dictionary<string, object>> recordSet, out string resultMessage)
|
||||
{
|
||||
recordSet = new List<Dictionary<string, object>>();
|
||||
resultMessage = string.Empty;
|
||||
|
||||
if (_con == null || _con.State != ConnectionState.Open)
|
||||
{
|
||||
resultMessage = "DBと接続されていません。";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var cmd = _con.CreateCommand())
|
||||
{
|
||||
// コパンドプロパティを注入
|
||||
if (!TryInjectSoulToCmd(cmd, sqlOrName, parameters, out var messageForMe))
|
||||
{
|
||||
resultMessage = messageForMe;
|
||||
return false;
|
||||
}
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row[reader.GetName(i)] = reader.GetValue(i);
|
||||
}
|
||||
recordSet.Add(row);
|
||||
}
|
||||
LoggerUxer.Info($"[Rec Count:{recordSet.Count}]");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"DBからデータ取得時にエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryInjectSoulToCmd(DbCommand cmd, string sqlOrName, Dictionary<string,object> parameters, out string resultMessage)
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
|
||||
// SQLの取得
|
||||
sqlOrName = sqlOrName.ToLower();
|
||||
var sql = _sqlDic.ContainsKey(sqlOrName) ? _sqlDic[sqlOrName] : sqlOrName;
|
||||
//LoggerUxer.Info($"[SQL:{sql}]");
|
||||
|
||||
// SQL中のコマンドパラメータ名取得
|
||||
var paraList = ExtractSqlParams(sql);
|
||||
//oggerUxer.Info($"[SQL Para names:{string.Join(",", paraList)}]");
|
||||
|
||||
// コマンドパラメータ値の型変換
|
||||
if (!TryChangeParamTypeFromSqlParams(ref parameters, ref paraList, out var messageForMe))
|
||||
{
|
||||
resultMessage = $"コマンドパラメータ値の型変換に失敗しました。[エラー内容:{messageForMe}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
// SQL文中のコマンドパラメータ型定義をカット
|
||||
cmd.CommandText = sql = CutParamTypeOfSQL(sql);
|
||||
//LoggerUxer.Info($"[SQL(Param Type Cut):{sql}]");
|
||||
|
||||
// 再度、SQL中のコマンドパラメータ名取得
|
||||
paraList = ExtractSqlParams(sql);
|
||||
//LoggerUxer.Info($"[SQL Para names(Type cut):{string.Join(",", paraList)}]");
|
||||
|
||||
// SQLに合わせたコマンドパラメータ登録
|
||||
foreach (var p in parameters)
|
||||
{
|
||||
if (!paraList.Contains(p.Key)) continue;
|
||||
var param = cmd.CreateParameter();
|
||||
param.ParameterName = p.Key;
|
||||
param.Value = p.Value;
|
||||
cmd.Parameters.Add(param);
|
||||
}
|
||||
|
||||
//LoggerUxer.Info($"[SQL Params:{GetCommandParametersCsv(cmd)}]");
|
||||
|
||||
if (cmd.Parameters.Count != paraList.Count)
|
||||
{
|
||||
resultMessage = $"必要なパラメータが不足しています。[SQL名:{sqlOrName},SQL項目:{string.Join(",", paraList)}, CSV項目:{string.Join(",", parameters.Keys)}]";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
_tran?.Commit();
|
||||
_isCommitted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 終了処理
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_tran != null && !_isCommitted)
|
||||
{
|
||||
Log.Warning("トランザクション未コミット → 自動ロールバックされました");
|
||||
_tran.Rollback(); // 明示的にロールバック
|
||||
}
|
||||
_con?.Dispose();
|
||||
_con = null;
|
||||
_tran = null;
|
||||
}
|
||||
|
||||
public static List<string> ExtractSqlParams(string sql)
|
||||
{
|
||||
//var matches = System.Text.RegularExpressions.Regex.Matches(sql, @":([a-zA-Z_][a-zA-Z0-9_]*)");
|
||||
//return matches.Cast<System.Text.RegularExpressions.Match>()
|
||||
// .Select(m => m.Groups[1].Value)
|
||||
// .Distinct()
|
||||
// .ToList();
|
||||
|
||||
//var result = new List<string>();
|
||||
//var lines = sql.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
|
||||
|
||||
//foreach (var line in lines)
|
||||
//{
|
||||
// // コメント部分を除去
|
||||
// var codePart = line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line;
|
||||
|
||||
// // パラメータ抽出
|
||||
// var matches = System.Text.RegularExpressions.Regex.Matches(
|
||||
// codePart,
|
||||
// @":(\[[a-zA-Z]\][a-zA-Z_][a-zA-Z0-9_]*)|:([a-zA-Z_][a-zA-Z0-9_]*)"
|
||||
// );
|
||||
// foreach (System.Text.RegularExpressions.Match match in matches)
|
||||
// {
|
||||
// var paramName = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
|
||||
// if (!result.Contains(paramName))
|
||||
// result.Add(paramName);
|
||||
// }
|
||||
//}
|
||||
//return result;
|
||||
|
||||
var result = new List<string>();
|
||||
var lines = sql.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// コメント部分を除去
|
||||
var codePart = line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line;
|
||||
|
||||
// パラメータ抽出(::で始まるものは除外)
|
||||
var matches = System.Text.RegularExpressions.Regex.Matches(
|
||||
codePart,
|
||||
@"(?<!:):(\[[a-zA-Z]\][a-zA-Z_][a-zA-Z0-9_]*)|(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)"
|
||||
);
|
||||
|
||||
foreach (System.Text.RegularExpressions.Match match in matches)
|
||||
{
|
||||
var paramName = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
|
||||
if (!result.Contains(paramName))
|
||||
result.Add(paramName);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public static string GetCommandParametersCsv(DbCommand command)
|
||||
{
|
||||
if (command == null || command.Parameters.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var list = new List<string>();
|
||||
|
||||
foreach (DbParameter param in command.Parameters)
|
||||
{
|
||||
string name = param.ParameterName?.TrimStart('@'); // @を除去(任意)
|
||||
object value = param.Value;
|
||||
|
||||
string formattedValue;
|
||||
|
||||
if (value == null || value == DBNull.Value)
|
||||
{
|
||||
formattedValue = "NULL";
|
||||
}
|
||||
else if (value is string || value is char)
|
||||
{
|
||||
formattedValue = $"'{value}'"; // シングルクォートで括る
|
||||
}
|
||||
else if (value is DateTime dt)
|
||||
{
|
||||
formattedValue = $"'{dt:yyyy-MM-dd HH:mm:ss}'"; // 日付も文字列扱い
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedValue = value.ToString();
|
||||
}
|
||||
|
||||
list.Add($"{name}={formattedValue}");
|
||||
}
|
||||
|
||||
return string.Join(",", list);
|
||||
}
|
||||
private static bool TryChangeParamTypeFromSqlParams(ref Dictionary<string, object> parameters, ref List<string> paraNameList, out string resultMessage)
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
|
||||
foreach (var key in parameters.Keys.ToList())
|
||||
{
|
||||
var name = paraNameList
|
||||
.FirstOrDefault(m => m == key || m.Substring(0, 1) == "[" && m.Substring(3) == key);
|
||||
if (name != null && name.Substring(0, 1) == "[")
|
||||
{
|
||||
switch (name.Substring(0, 3).ToUpper())
|
||||
{
|
||||
case "[I]":
|
||||
if (string.IsNullOrEmpty(parameters[key].ToString()))
|
||||
{
|
||||
parameters[key] = DBNull.Value;
|
||||
}
|
||||
else if (int.TryParse(parameters[key].ToString(), out int intValue))
|
||||
{
|
||||
parameters[key] = intValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessage = $"パラメータ値[I]型変換に失敗しました。[キー:{key}値:{parameters[key]}]";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
//case "[IN":
|
||||
// if (string.IsNullOrEmpty(parameters[key].ToString()))
|
||||
// {
|
||||
// parameters[key] = DBNull.Value;
|
||||
// }
|
||||
// else if (int.TryParse(parameters[key].ToString(), out intValue))
|
||||
// {
|
||||
// parameters[key] = intValue;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// resultMessage = $"パラメータ値[IN]型変換に失敗しました。[キー:{key}値:{parameters[key]}]";
|
||||
// return false;
|
||||
// }
|
||||
// break;
|
||||
case "[F]":
|
||||
if (double.TryParse(parameters[key].ToString(), out double doubleValue))
|
||||
{
|
||||
parameters[key] = doubleValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessage = $"パラメータ値[F]型変換に失敗しました。[キー:{key}値:{parameters[key]}]";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "[D]":
|
||||
if (DateTime.TryParse(parameters[key].ToString(), out DateTime dtValue))
|
||||
{
|
||||
parameters[key] = dtValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessage = $"パラメータ値[D]型変換に失敗しました。[キー:{key}値:{parameters[key]}]";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "[S]":
|
||||
parameters[key] = parameters[key].ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// SQL文中のコマンドパラメータ型定義をカット
|
||||
private static string CutParamTypeOfSQL(string sql)
|
||||
{
|
||||
string[] cutItems = new string[] { "[I]", "[F]", "[D]", "[S]" };
|
||||
foreach (var cutItem in cutItems)
|
||||
{
|
||||
sql = sql.Replace(cutItem.ToUpper(), "");
|
||||
sql = sql.Replace(cutItem.ToLower(), "");
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KssSmaPlaLib.IO.Excel
|
||||
{
|
||||
public static class ExcelCellValidatorUxer
|
||||
{
|
||||
/// <summary>
|
||||
/// ExcelのA1形式(例: "A1", "Z99", "AA1000")かどうかを判定します。
|
||||
/// </summary>
|
||||
/// <param name="cellRef">セル参照文字列</param>
|
||||
/// <returns>有効なA1形式ならtrue、無効ならfalse</returns>
|
||||
public static bool IsValidA1Format(string cellRef)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cellRef))
|
||||
return false;
|
||||
|
||||
// 正規表現でA1形式をチェック(列: A〜XFD、行: 1〜1048576)
|
||||
var match = System.Text.RegularExpressions.Regex.Match(
|
||||
cellRef.ToUpperInvariant(),
|
||||
@"^([A-Z]{1,3})([1-9][0-9]{0,6})$"
|
||||
);
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
string colPart = match.Groups[1].Value;
|
||||
string rowPart = match.Groups[2].Value;
|
||||
|
||||
// 列がXFD(Excel最大列)以内かチェック
|
||||
if (ColumnNameToNumber(colPart) > 16384)
|
||||
return false;
|
||||
|
||||
// 行がExcel最大行以内かチェック
|
||||
if (int.TryParse(rowPart, out int row))
|
||||
{
|
||||
return row >= 1 && row <= 1048576;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列名(例: "A", "Z", "AA")を列番号に変換(例: 1, 26, 27)
|
||||
/// </summary>
|
||||
public static int ColumnNameToNumber(string columnName)
|
||||
{
|
||||
int sum = 0;
|
||||
foreach (char c in columnName)
|
||||
{
|
||||
sum *= 26;
|
||||
sum += (c - 'A' + 1);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KssSmaPlaLib.IO.Excel
|
||||
{
|
||||
public static class ExcelColumnHelperUxer
|
||||
{
|
||||
public static string GetExcelColumnLetter(int columnNumber)
|
||||
{
|
||||
if (columnNumber < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(columnNumber), "Column number must be >= 1");
|
||||
|
||||
string columnLetter = string.Empty;
|
||||
while (columnNumber > 0)
|
||||
{
|
||||
columnNumber--; // Adjust for 0-indexed calculation
|
||||
columnLetter = (char)('A' + (columnNumber % 26)) + columnLetter;
|
||||
columnNumber /= 26;
|
||||
}
|
||||
return columnLetter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using ClosedXML.Excel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KssSmaPlaLib.IO.Excel
|
||||
{
|
||||
public class XlUxer
|
||||
{
|
||||
public static IXLCell GCell(IXLWorksheet sheet, string a1Address)
|
||||
{
|
||||
var cell = sheet.Cell(a1Address);
|
||||
return GCell(cell);
|
||||
}
|
||||
|
||||
public static IXLCell GCell(IXLWorksheet sheet, int row, int col)
|
||||
{
|
||||
var cell = sheet.Cell(row, col);
|
||||
return GCell(cell);
|
||||
}
|
||||
|
||||
public static IXLCell GCell(IXLCell cell)
|
||||
{
|
||||
return cell.IsMerged() ? cell.MergedRange().FirstCell() : cell;
|
||||
}
|
||||
|
||||
public static bool TryValidateA1CellAddress(string a1Address, out string normalizedAddress)
|
||||
{
|
||||
normalizedAddress = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(a1Address))
|
||||
return false;
|
||||
|
||||
// 正規表現でA1形式(例:A1, B2, AA10など)をチェック
|
||||
var regex = new Regex(@"^[A-Z]{1,3}[1-9][0-9]*$", RegexOptions.IgnoreCase);
|
||||
if (!regex.IsMatch(a1Address))
|
||||
return false;
|
||||
|
||||
// 正規化(大文字化)
|
||||
normalizedAddress = a1Address.ToUpperInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shorter
|
||||
/// </summary>
|
||||
/// <param name="colLetter"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetColNo(string colLetter)
|
||||
{
|
||||
if (!TryConvertColLetterToNumber(colLetter, out var no))
|
||||
{
|
||||
return int.MinValue;
|
||||
}
|
||||
return no;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列アルファベットを列番号(1オリジン)に変換
|
||||
/// </summary>
|
||||
/// <param name="colLetter">列アルファベット</param>
|
||||
/// <param name="colNo">[出力]列番号(1オリジン)</param>
|
||||
/// <returns>成否</returns>
|
||||
public static bool TryConvertColLetterToNumber(string colLetter, out int colNo)
|
||||
{
|
||||
colNo = int.MinValue;
|
||||
|
||||
// バリデ(A~XFD)
|
||||
if(!Regex.IsMatch(colLetter, @"^(?i)([A-Z]|[A-Z]{2}|[A-E][A-Z]{2})$"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 大文字化
|
||||
colLetter = colLetter.ToUpper();
|
||||
|
||||
// アルファベット1文字ずつ見て列番号を算出
|
||||
int no = 0;
|
||||
foreach (char c in colLetter)
|
||||
{
|
||||
no = no * 26 + (c - 'A' + 1);
|
||||
}
|
||||
|
||||
// 列番号範囲外なら失敗
|
||||
if (no > 16384 || no < 1) return false;
|
||||
|
||||
// 成功
|
||||
colNo = no;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using KssSmaPlaLib.Commons;
|
||||
using KssSmaPlaLib.Exchange.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace KssSmaPlaLib.IO.File
|
||||
{
|
||||
public class CsvUxer
|
||||
{
|
||||
public static bool ExportToCsv<T>(List<T> list, string filePath, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) where T : ICsvExportableUxer
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
var lines = new List<string> { list.FirstOrDefault()?.CsvHeader ?? "" };
|
||||
lines.AddRange(list.Select(item => item.ToCsvLine()));
|
||||
System.IO.File.WriteAllLines(filePath, lines, EncodingUxer.GetEncoding(encodingType));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"CSV出力に失敗しました。[ファイルパス:{filePath},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSVインポート
|
||||
/// </summary>
|
||||
/// <param name="csvFilePath">CSVファイルパス</param>
|
||||
/// <param name="recordSet">[出力]レコードセット</param>
|
||||
/// <param name="resultMessage">[出力]メッセージ</param>
|
||||
/// <param name="encodingType">[オプション]エンコード</param>
|
||||
/// <returns>成否</returns>
|
||||
public static bool ImportFromCsv(string csvFilePath, out List<Dictionary<string, string>> recordSet, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom)
|
||||
{
|
||||
recordSet = new List<Dictionary<string, string>>();
|
||||
resultMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// 全行読み込み
|
||||
var lines = System.IO.File.ReadAllLines(csvFilePath, EncodingUxer.GetEncoding(encodingType));
|
||||
|
||||
// データ行0件
|
||||
if (lines.Length < 2) return true;
|
||||
|
||||
// ヘッダ行項目分割
|
||||
var header = Regex.Split(lines[0], ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
|
||||
for (var cnt = 0; cnt < header.Length; cnt++) header[cnt] = header[cnt].Trim('\"');
|
||||
|
||||
// データ行件数分
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
// 項目分割
|
||||
var values = Regex.Split(lines[i], ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
|
||||
for (var cnt = 0; cnt < values.Length; cnt++) values[cnt] = values[cnt].Trim('\"');
|
||||
|
||||
if (values.Length != header.Length)
|
||||
{
|
||||
resultMessage = $"CSV読み込みでヘッダの項目数とデータの項目数が異なります。[CSVファイルパス:{csvFilePath},行番号:{i + 1},ヘッダ行項目数:{header.Length},データ行項目数:{values.Length}]";
|
||||
return false;
|
||||
}
|
||||
|
||||
var record = new Dictionary<string, string>();
|
||||
|
||||
// 全項目
|
||||
for (int j = 0; j < header.Length && j < values.Length; j++)
|
||||
{
|
||||
var key = header[j].Trim('\"');
|
||||
var value = values[j].Trim('\"');
|
||||
record[key] = value;
|
||||
}
|
||||
recordSet.Add(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"CSV読み込み処理でエラーが発生しました。[CSVファイルパス:{csvFilePath},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Escape(string value) => $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
public static string Join(params object[] values) => string.Join(",", values.Select(m => Escape(m == null ? "" : m.ToString())));
|
||||
|
||||
/// <summary>
|
||||
/// CSVファイルを読み込み、ディクショナリに変換します。
|
||||
/// </summary>
|
||||
/// <param name="path">CSVファイルのパス</param>
|
||||
/// <param name="dic">出力されるディクショナリ(ファイル無しの場合は空)</param>
|
||||
/// <param name="resultMessage"></param>
|
||||
/// <returns>処理が成功した場合はtrue、例外発生時はfalse</returns>
|
||||
public static bool TryGetDictionaryFromCsv(
|
||||
string path,
|
||||
out Dictionary<string, string> dic,
|
||||
out string resultMessage)
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
|
||||
// 1. 最初に出力変数を空のディクショナリで初期化
|
||||
dic = new Dictionary<string, string>();
|
||||
|
||||
try
|
||||
{
|
||||
// 2. ファイルが存在しない場合は空のままtrueで返す
|
||||
if (!System.IO.File.Exists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. BOM付きUTF-8を指定してファイルを開く
|
||||
// (Encoding.UTF8 はデフォルトでBOMの有無を自動判別して正しく読み込めます)
|
||||
using (var reader = new StreamReader(path, Encoding.UTF8))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
string line = reader.ReadLine();
|
||||
|
||||
// 空行はスキップ
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// カンマで分割 (単純な2列前提)
|
||||
string[] parts = line.Split(',');
|
||||
|
||||
// 最低1列(キー)があれば処理する(2列未満の対策)
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
string key = parts[0];
|
||||
|
||||
// 2列目があればその値、なければ空文字
|
||||
string value = parts.Length > 1 ? parts[1] : string.Empty;
|
||||
|
||||
// 重複キーは後勝ち(インデクサを使うことで上書き保存)
|
||||
dic[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: あたしがのちほどエラーメッセージ処理を入れる!
|
||||
resultMessage = $"CSVファイル読み込みに失敗しました。[詳細:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using DocumentFormat.OpenXml.Vml;
|
||||
using KssSmaPlaLib.Commons;
|
||||
using KssSmaPlaLib.Exchange.Interfaces;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace KssSmaPlaLib.IO.File
|
||||
{
|
||||
public static class FileUxer
|
||||
{
|
||||
/// <summary>
|
||||
/// テキストファイル出力(全テキスト一括)
|
||||
/// </summary>
|
||||
/// <param name="allLineText">全テキスト</param>
|
||||
/// <param name="filePath">ファイルパス</param>
|
||||
/// <param name="resultMessage">メッセージ</param>
|
||||
/// <param name="encodingType">文字コード</param>
|
||||
/// <returns>成否</returns>
|
||||
public static bool WriteAllText(string allLineText, string filePath, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom)
|
||||
{
|
||||
resultMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
System.IO.File.WriteAllText(filePath, allLineText, EncodingUxer.GetEncoding(encodingType));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"CSV出力に失敗しました。[ファイルパス:{filePath},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryDeletePatternFiles(
|
||||
string folderPath,
|
||||
string fileNamePattern,
|
||||
out int fileCount,
|
||||
out string resultMessage)
|
||||
{
|
||||
fileCount = 0;
|
||||
resultMessage = string.Empty;
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
resultMessage = $"フォルダが存在しません。[フォルダパス:{folderPath}]";
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(folderPath, fileNamePattern);
|
||||
|
||||
foreach (var filePath in files)
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
fileCount++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"ファイル削除中にエラーが発生しました。[フォルダパス:{folderPath},ファイル名パターン:{fileNamePattern},エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetTimestampedFileName(string originalName, DateTime dt, string prefixDtFormat, string suffixDtFormat)
|
||||
{
|
||||
string nameWithoutExt = Path.GetFileNameWithoutExtension(originalName);
|
||||
string ext = Path.GetExtension(originalName);
|
||||
string prefixDtStr = dt.ToString(prefixDtFormat);
|
||||
string suffixDtStr = dt.ToString(suffixDtFormat);
|
||||
|
||||
string newName = prefixDtStr + nameWithoutExt + suffixDtStr;
|
||||
return newName + ext;
|
||||
}
|
||||
|
||||
public static bool CheckParentFolderExists(string folderPath, out string parentPath)
|
||||
{
|
||||
parentPath = Path.GetDirectoryName(folderPath);
|
||||
return Directory.Exists(parentPath);
|
||||
}
|
||||
|
||||
public static bool TryApplyActionToFiles<T>(
|
||||
string folderPath,
|
||||
Func<FileInfo, bool> condition,
|
||||
Action<FileInfo> action,
|
||||
out int fileCount,
|
||||
out string resultMessage,
|
||||
string actionName = "操作",
|
||||
bool isDescending = true,
|
||||
Func<FileInfo, T> sortKey = null,
|
||||
int skip = 0)
|
||||
{
|
||||
fileCount = 0;
|
||||
resultMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// 条件未指定時は全件対象
|
||||
condition = condition ?? (_ => true);
|
||||
|
||||
// 一旦条件で取得
|
||||
var files = new DirectoryInfo(folderPath)
|
||||
.GetFiles()
|
||||
.Where(condition);
|
||||
|
||||
// ソートキー作成
|
||||
var keySelector =
|
||||
sortKey != null ? (Func<FileInfo, object>)
|
||||
(f => sortKey(f)) : f => f.LastWriteTime;
|
||||
|
||||
// ソート+スキップ+リスト化
|
||||
var filesList =
|
||||
(isDescending ?
|
||||
files.OrderByDescending(keySelector) :
|
||||
files.OrderBy(keySelector))
|
||||
.Skip(skip)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in filesList)
|
||||
{
|
||||
action(file);
|
||||
LoggerUxer.Info($"{actionName}しました。[{file}]");
|
||||
}
|
||||
|
||||
fileCount = filesList.Count;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultMessage = $"ファイル群の{actionName}に失敗しました。[エラー内容:{ex.Message}]";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KssSmaPlaLib.IO.File
|
||||
{
|
||||
public static class JsonFileUxer
|
||||
{
|
||||
/// <summary>
|
||||
/// JSONファイルのピンポイント1項目更新
|
||||
/// </summary>
|
||||
/// <param name="path">JSONファイルパス</param>
|
||||
/// <param name="jsonPath">JSONノードパス</param>
|
||||
/// <param name="newValue">更新値</param>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static void UpdateJsonKeyAtomic(string path, string jsonPath, object newValue)
|
||||
{
|
||||
//// 使い方例:パスワード更新
|
||||
//JsonFiileUxer.UpdateJsonKeyAtomic("config.json", "app.auth.password", "new_pw_2026-01");
|
||||
|
||||
var jtoken = JToken.FromObject(newValue);
|
||||
|
||||
// 1) 読み込み(UTF-8)
|
||||
var json = System.IO.File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
var jo = JObject.Parse(json);
|
||||
|
||||
// 2) ピンポイント更新(途中が無ければ作る)
|
||||
var token = jo.SelectToken(jsonPath);
|
||||
if (token is null)
|
||||
{
|
||||
// 例: "app.auth.password" を分解して動的に作る
|
||||
var parts = jsonPath.Split('.');
|
||||
JToken cur = jo;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var name = parts[i];
|
||||
var obj = cur as JObject ?? throw new InvalidOperationException("親がオブジェクトではありません");
|
||||
if (i == parts.Length - 1)
|
||||
{
|
||||
obj[name] = jtoken;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (obj[name] == null) obj[name] = new JObject();
|
||||
cur = obj[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
token.Replace(jtoken);
|
||||
}
|
||||
|
||||
// 3) 一時ファイルへ書き出し
|
||||
var dir = System.IO.Path.GetDirectoryName(path) ?? ".";
|
||||
var tmp = System.IO.Path.Combine(dir, ".tmp_config.json");
|
||||
var backup = path + $"_{DateTime.Now.ToString("yyyyMMddHHmmss")}" + ".bak";
|
||||
|
||||
//var output = jo.ToString(Formatting.Indented) + Environment.NewLine;
|
||||
var output = JsonConvert.SerializeObject(jo, Formatting.Indented) + Environment.NewLine;
|
||||
System.IO.File.WriteAllText(tmp, output, new UTF8Encoding(false));
|
||||
|
||||
// 4) バックアップ作成 → 原子置換
|
||||
if (System.IO.File.Exists(path)) System.IO.File.Copy(path, backup, overwrite: true);
|
||||
System.IO.File.Replace(tmp, path, backup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSONファイルから、jsonPathに一致する単一項目を取得します。
|
||||
/// 取得に成功したら true を返し、 out value に JToken を設定します。
|
||||
/// キーが見つからない・JSON無効・ファイル無し等は false を返し、 value は null。
|
||||
/// </summary>
|
||||
public static bool TryGetJsonKey(string path, string jsonPath, out JToken value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
// 1) 読み込み(UTF-8 BOMなし)
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = System.IO.File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ファイルがない/読み取り不可
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) 解析
|
||||
JObject jo;
|
||||
try
|
||||
{
|
||||
jo = JObject.Parse(json);
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
// JSON不正
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3) ピンポイント取得
|
||||
// 例: "app.auth.password" / "items[0].name" / "users['alice'].age" 等
|
||||
try
|
||||
{
|
||||
var token = jo.SelectToken(jsonPath);
|
||||
if (token is null)
|
||||
{
|
||||
return false; // キー未存在
|
||||
}
|
||||
|
||||
value = token; // JValue/JObject/JArray いずれもOK
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// JSONPathの書式誤り(SelectTokenが例外)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文字列として取得(JToken型を意識したくない場合の軽量ヘルパー)。
|
||||
/// </summary>
|
||||
public static bool TryGetJsonString(string path, string jsonPath, out string result)
|
||||
{
|
||||
result = null;
|
||||
if (!TryGetJsonKey(path, jsonPath, out var token)) return false;
|
||||
|
||||
// JValue以外(オブジェクト/配列)の場合は ToString() を許容するか判断(ここではJValueのみ厳密)
|
||||
if (token is JValue jv)
|
||||
{
|
||||
// 文字列ならそのまま
|
||||
if (jv.Type == JTokenType.String)
|
||||
{
|
||||
result = (string)jv.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 数値・真偽・Null などは ToString() で返す設計でもOK
|
||||
// ここでは文字列のみ厳密に true とします(お好みで分岐調整)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 符号付き整数(Int64)で取得(intの範囲超えも想定)。
|
||||
/// </summary>
|
||||
public static bool TryGetJsonInt64(string path, string jsonPath, out long result)
|
||||
{
|
||||
result = default;
|
||||
if (!TryGetJsonKey(path, jsonPath, out var token)) return false;
|
||||
|
||||
if (token is JValue jv)
|
||||
{
|
||||
// JValueが数値型なら long へ
|
||||
if (jv.Type == JTokenType.Integer)
|
||||
{
|
||||
// Newtonsoftは内部的に long を持つのでキャスト可能
|
||||
try
|
||||
{
|
||||
result = Convert.ToInt64(jv.Value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 文字列数値を許可する場合("42" → 42)
|
||||
if (jv.Type == JTokenType.String && long.TryParse((string)jv.Value, out var parsed))
|
||||
{
|
||||
result = parsed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任意型の既定値付き取得(存在しなければ defaultValue を返す)。
|
||||
/// 例:GetJsonKeyOrDefault(path, "app.auth.retries", 3)
|
||||
/// </summary>
|
||||
public static T GetJsonKeyOrDefault<T>(string path, string jsonPath, T defaultValue)
|
||||
{
|
||||
if (!TryGetJsonKey(path, jsonPath, out var token) || token is null)
|
||||
return defaultValue;
|
||||
|
||||
try
|
||||
{
|
||||
// JToken → 任意型に変換(失敗時は既定値)
|
||||
var val = token.ToObject<T>();
|
||||
return val == null ? defaultValue : val;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using KssSmaPlaLib.Commons;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KssSmaPlaLib.IO.File
|
||||
{
|
||||
public static class PathUxer
|
||||
{
|
||||
public static string CreateFullPathEx(
|
||||
string folderPath,
|
||||
string fileName,
|
||||
string dtPrefix = "",
|
||||
string dtSuffix = "")
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
// フォルダ:未指定時はカレントフォルダ
|
||||
folderPath = string.IsNullOrEmpty(folderPath) ? "." : folderPath;
|
||||
|
||||
// ファイル名:未指定時は"noname.txt"とする
|
||||
fileName = string.IsNullOrEmpty(fileName) ? "noname.txt" : fileName;
|
||||
|
||||
if (fileName.IndexOf('.') == -1)
|
||||
{
|
||||
// 拡張子がないとき、付与する
|
||||
fileName += ".txt";
|
||||
}
|
||||
|
||||
// ファイルパス
|
||||
var path = Path.Combine(
|
||||
Path.GetFullPath(folderPath),
|
||||
(string.IsNullOrEmpty(dtPrefix) ? "" : now.ToString(dtPrefix)) +
|
||||
fileName.Split('.')[0] +
|
||||
(string.IsNullOrEmpty(dtSuffix) ? "" : now.ToString(dtSuffix)) +
|
||||
"." +
|
||||
fileName.Split('.')[1]);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static string CombineUnixPath(params string[] parts)
|
||||
{
|
||||
return string.Join("/", parts.Select(p => p.Trim('/')));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ファイル名(パス不可)を受け取り、存在すればフルパスを返す。
|
||||
/// 探索順:①このDLLのフォルダ → ②エントリEXEのフォルダ → ③AppDomain.BaseDirectory
|
||||
/// (C)昼コパソフトウェア㈱
|
||||
/// </summary>
|
||||
/// <param name="fileName">パスではないファイル名のみ(例: "config.json")。</param>
|
||||
/// <param name="fullPath">見つかったファイルのフルパス。未発見時は null。</param>
|
||||
/// <returns>発見した場合 true、未発見の場合 false。</returns>
|
||||
public static bool TryResolveInAssemblyFolder(string fileName, out string fullPath)
|
||||
{
|
||||
fullPath = null;
|
||||
|
||||
// --- ガード:パスを排除(「ファイル名のみ」の前提を厳守) ---
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return false;
|
||||
|
||||
// ディレクトリ成分が入っていたら拒否
|
||||
if (!string.Equals(fileName, Path.GetFileName(fileName), StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
// 予約文字の簡易チェック(Windows前提の一般的なNG文字)
|
||||
foreach (var ch in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
if (fileName.IndexOf(ch) >= 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- ① このメソッドを含むライブラリ(DLL)のフォルダ ---
|
||||
var dllFolder = SafeGetDirectoryName(SafeGetAssemblyLocation(Assembly.GetExecutingAssembly()));
|
||||
if (TryCombineAndExists(dllFolder, fileName, out fullPath))
|
||||
return true;
|
||||
|
||||
// --- ② エントリアセンブリ(EXE)のフォルダ ---
|
||||
string exeFolder = null;
|
||||
var entry = Assembly.GetEntryAssembly();
|
||||
if (entry != null)
|
||||
{
|
||||
exeFolder = SafeGetDirectoryName(SafeGetAssemblyLocation(entry));
|
||||
}
|
||||
else
|
||||
{
|
||||
// サービスや特殊ホストで EntryAssembly が null の場合のフォールバック
|
||||
try
|
||||
{
|
||||
exeFolder = SafeGetDirectoryName(Process.GetCurrentProcess()?.MainModule?.FileName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 取れないケースもあるので無理せず次へ
|
||||
exeFolder = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (TryCombineAndExists(exeFolder, fileName, out fullPath))
|
||||
return true;
|
||||
|
||||
// --- ③ AppDomain 基準(ClickOnce等のケースでも安定) ---
|
||||
var baseDir = AppDomain.CurrentDomain?.BaseDirectory;
|
||||
if (TryCombineAndExists(baseDir, fileName, out fullPath))
|
||||
return true;
|
||||
|
||||
// --- 未発見 ---
|
||||
fullPath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指定フォルダに fileName を結合し、存在すれば out に設定して true。
|
||||
/// </summary>
|
||||
private static bool TryCombineAndExists(string folder, string fileName, out string path)
|
||||
{
|
||||
path = null;
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var candidate = Path.Combine(folder, fileName);
|
||||
if (System.IO.File.Exists(candidate))
|
||||
{
|
||||
path = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// パス結合や存在確認で例外が出てもスキップして未発見扱い
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assembly.Location を安全に取得(Reflection-only / 影響ケースを考慮)。
|
||||
/// </summary>
|
||||
private static string SafeGetAssemblyLocation(Assembly asm)
|
||||
{
|
||||
try
|
||||
{
|
||||
return asm?.Location;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DirectoryName を安全に取得。
|
||||
/// </summary>
|
||||
private static string SafeGetDirectoryName(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.IsNullOrEmpty(path) ? null : Path.GetDirectoryName(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user