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(File.ReadAllText(path)); _password = cfg?.password ?? ""; } [Serializable] private class NasConfig { public string password; } // ── Public API ─────────────────────────────────────────── public IEnumerator Publish( SongInfo song, string audioPath, Dictionary> maps, Action onProgress, Action onComplete, Action 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 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 onError) { byte[] bytes = File.ReadAllBytes(localPath); yield return UploadBytes(bytes, fileName, nasFolder, onError); } private IEnumerator UploadBytes(byte[] bytes, string fileName, string nasFolder, Action 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 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(req.downloadHandler.text); } } if (songsList == null) songsList = new SongsList { version = "1.0", songs = new System.Collections.Generic.List() }; // 같은 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}"; } }