Files

256 lines
9.8 KiB
C#
Raw Permalink Normal View History

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
// Synology DSM File Station API를 통해 NAS에 파일 업로드 및 songs.json 갱신
public class NasPublisher : MonoBehaviour
{
[Header("NAS 접속 정보")]
[SerializeField] private string nasBaseUrl = "http://192.168.55.3:5000";
[SerializeField] private string nasAccount = "admin";
[SerializeField] private string nasRootPath = "/web/beatsaber";
[Header("정적 서버 URL (songs.json 읽기용)")]
[SerializeField] private string staticBaseUrl = "http://whdwo798.synology.me/beatsaber";
private string _sid = "";
private string _synoToken = ""; // DSM 7 CSRF 토큰 (enable_syno_token=yes 로 획득)
private string _password = ""; // StreamingAssets/nas_config.json 에서 로드
private void Awake()
{
LoadConfig();
}
private void LoadConfig()
{
string path = Path.Combine(Application.streamingAssetsPath, "nas_config.json");
if (!File.Exists(path))
{
Debug.LogWarning("[NasPublisher] nas_config.json 없음: " + path);
return;
}
var cfg = JsonUtility.FromJson<NasConfig>(File.ReadAllText(path));
_password = cfg?.password ?? "";
}
[Serializable]
private class NasConfig { public string password; }
// ── Public API ───────────────────────────────────────────
public IEnumerator Publish(
SongInfo song,
string audioPath,
Dictionary<string, List<NoteData>> maps,
Action<float> onProgress,
Action onComplete,
Action<string> onError)
{
bool failed = false;
void OnErr(string e) { onError?.Invoke(e); failed = true; }
// 1단계: DSM 로그인
yield return Login(OnErr);
if (string.IsNullOrEmpty(_sid)) yield break;
onProgress?.Invoke(0.1f);
// 2단계: 오디오 업로드
yield return UploadFile(audioPath, $"{nasRootPath}/music", $"{song.id}.mp3", OnErr);
if (failed) { yield return Logout(); yield break; }
onProgress?.Invoke(0.4f);
// 3단계: 각 난이도 맵 JSON 업로드
int total = maps.Count;
int done = 0;
foreach (var kv in maps)
{
string fileName = $"Map_{song.id}_{kv.Key}.json";
string json = BeatSageConverter.ToMapJson(kv.Value);
byte[] bytes = Encoding.UTF8.GetBytes(json);
AssignMapFile(song, kv.Key, fileName);
yield return UploadBytes(bytes, fileName, $"{nasRootPath}/maps", OnErr);
if (failed) { yield return Logout(); yield break; }
done++;
onProgress?.Invoke(0.4f + (float)done / total * 0.3f);
}
// 4단계: songs.json 다운로드 → 항목 추가 → 재업로드
yield return PatchSongsJson(song, OnErr);
if (failed) { yield return Logout(); yield break; }
onProgress?.Invoke(0.95f);
// 5단계: 로그아웃
yield return Logout();
onProgress?.Invoke(1f);
onComplete?.Invoke();
Debug.Log($"[NasPublisher] '{song.title}' NAS 업로드 완료");
}
// ── DSM 인증 ─────────────────────────────────────────────
private IEnumerator Login(Action<string> onError)
{
Debug.Log($"[NasPublisher] 로그인 시도 — nasBaseUrl: '{nasBaseUrl}'");
string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=6&method=login" +
$"&account={UnityWebRequest.EscapeURL(nasAccount)}" +
$"&passwd={UnityWebRequest.EscapeURL(_password)}" +
$"&session=FileStation&format=sid&enable_syno_token=yes";
using var req = UnityWebRequest.Get(url);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"DSM 로그인 실패: {req.error}");
yield break;
}
Debug.Log($"[NasPublisher] 로그인 응답: {req.downloadHandler.text}");
string resp = req.downloadHandler.text;
_sid = ParseJsonString(resp, "sid");
_synoToken = ParseJsonString(resp, "synotoken");
Debug.Log($"[NasPublisher] sid={_sid}, synotoken={_synoToken}");
if (string.IsNullOrEmpty(_sid))
onError?.Invoke("DSM sid 파싱 실패 — 계정 정보를 확인하세요");
}
private IEnumerator Logout()
{
string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=1&method=logout&session=FileStation&_sid={_sid}";
using var req = UnityWebRequest.Get(url);
yield return req.SendWebRequest();
_sid = "";
}
// ── 파일 업로드 ───────────────────────────────────────────
private IEnumerator UploadFile(string localPath, string nasFolder,
string fileName, Action<string> onError)
{
byte[] bytes = File.ReadAllBytes(localPath);
yield return UploadBytes(bytes, fileName, nasFolder, onError);
}
private IEnumerator UploadBytes(byte[] bytes, string fileName,
string nasFolder, Action<string> onError)
{
Debug.Log($"[NasPublisher] 업로드 시도 — path: '{nasFolder}', file: '{fileName}'");
string uploadUrl = $"{nasBaseUrl}/webapi/entry.cgi" +
$"?api=SYNO.FileStation.Upload&version=2&method=upload" +
$"&_sid={UnityWebRequest.EscapeURL(_sid)}";
// PowerShell 테스트와 동일한 방식으로 multipart body 수동 구성
string boundary = System.Guid.NewGuid().ToString("N");
const string CRLF = "\r\n";
using var bodyStream = new MemoryStream();
void WriteText(string text)
{
var b = Encoding.UTF8.GetBytes(text);
bodyStream.Write(b, 0, b.Length);
}
void WriteField(string name, string value)
{
WriteText($"--{boundary}{CRLF}");
WriteText($"Content-Disposition: form-data; name=\"{name}\"{CRLF}{CRLF}");
WriteText(value);
WriteText(CRLF);
}
WriteField("path", nasFolder);
WriteField("create_parents", "true");
WriteField("overwrite", "true");
WriteText($"--{boundary}{CRLF}");
WriteText($"Content-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"{CRLF}");
WriteText($"Content-Type: application/octet-stream{CRLF}{CRLF}");
bodyStream.Write(bytes, 0, bytes.Length);
WriteText(CRLF);
WriteText($"--{boundary}--{CRLF}");
byte[] bodyBytes = bodyStream.ToArray();
using var req = new UnityWebRequest(uploadUrl, "POST");
req.uploadHandler = new UploadHandlerRaw(bodyBytes);
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", $"multipart/form-data; boundary={boundary}");
if (!string.IsNullOrEmpty(_synoToken))
req.SetRequestHeader("X-SYNO-TOKEN", _synoToken);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"업로드 실패({fileName}): {req.error}");
yield break;
}
Debug.Log($"[NasPublisher] 업로드 응답({fileName}): {req.downloadHandler.text}");
if (req.downloadHandler.text.Contains("\"success\":false"))
onError?.Invoke($"업로드 거부({fileName}): {req.downloadHandler.text}");
}
// ── songs.json 패치 ───────────────────────────────────────
private IEnumerator PatchSongsJson(SongInfo newSong, Action<string> onError)
{
// 현재 songs.json 가져오기 (정적 서버에서 읽음)
SongsList songsList = null;
using (var req = UnityWebRequest.Get($"{staticBaseUrl}/songs.json"))
{
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
songsList = JsonUtility.FromJson<SongsList>(req.downloadHandler.text);
}
}
if (songsList == null)
songsList = new SongsList { version = "1.0", songs = new System.Collections.Generic.List<SongInfo>() };
// 같은 id가 이미 있으면 교체, 없으면 추가
int existingIdx = songsList.songs.FindIndex(s => s.id == newSong.id);
if (existingIdx >= 0)
songsList.songs[existingIdx] = newSong;
else
songsList.songs.Add(newSong);
// 수정된 songs.json 업로드
byte[] jsonBytes = Encoding.UTF8.GetBytes(JsonUtility.ToJson(songsList, true));
yield return UploadBytes(jsonBytes, "songs.json", nasRootPath, onError);
}
// ── 유틸 ─────────────────────────────────────────────────
private static string ParseJsonString(string json, string key)
{
string search = $"\"{key}\":\"";
int start = json.IndexOf(search, StringComparison.Ordinal);
if (start < 0) return null;
start += search.Length;
int end = json.IndexOf('"', start);
return end > start ? json.Substring(start, end - start) : null;
}
private static void AssignMapFile(SongInfo song, string difficulty, string fileName)
{
var info = song.difficulties.Get(difficulty);
if (info != null) info.mapFile = $"maps/{fileName}";
}
}