207 lines
7.5 KiB
C#
207 lines
7.5 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|
||
}
|