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"; // DSM 포트 (내부망) [SerializeField] private string nasAccount = "admin"; [SerializeField] private string nasPassword = ""; // Inspector에서 입력 [SerializeField] private string nasRootPath = "/web/beatsaber"; // NAS 내 절대경로 [Header("정적 서버 URL (songs.json 읽기용)")] [SerializeField] private string staticBaseUrl = "http://whdwo798.synology.me:8180/beatsaber"; private string _sid = ""; // DSM 세션 ID // ── Public API ─────────────────────────────────────────── public IEnumerator Publish( SongInfo song, string audioPath, Dictionary> maps, Action onProgress, Action onComplete, Action onError) { // 1단계: DSM 로그인 yield return Login(onError); if (string.IsNullOrEmpty(_sid)) yield break; onProgress?.Invoke(0.1f); // 2단계: 오디오 업로드 yield return UploadFile( audioPath, $"{nasRootPath}/music", $"{song.id}.mp3", onError); 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); // 파일명에 맞춰 DifficultyInfo 업데이트 AssignMapFile(song, kv.Key, fileName); yield return UploadBytes( bytes, fileName, $"{nasRootPath}/maps", onError); done++; onProgress?.Invoke(0.4f + (float)done / total * 0.3f); } // 4단계: songs.json 다운로드 → 항목 추가 → 재업로드 yield return PatchSongsJson(song, onError); 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) { string url = $"{nasBaseUrl}/webapi/auth.cgi" + $"?api=SYNO.API.Auth&version=3&method=login" + $"&account={UnityWebRequest.EscapeURL(nasAccount)}" + $"&passwd={UnityWebRequest.EscapeURL(nasPassword)}" + $"&session=FileStation&format=sid"; using var req = UnityWebRequest.Get(url); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { onError?.Invoke($"DSM 로그인 실패: {req.error}"); yield break; } _sid = ParseSid(req.downloadHandler.text); 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) { string url = $"{nasBaseUrl}/webapi/entry.cgi"; var form = new List { new MultipartFormDataSection("api", "SYNO.FileStation.Upload"), new MultipartFormDataSection("version", "2"), new MultipartFormDataSection("method", "upload"), new MultipartFormDataSection("path", nasFolder), new MultipartFormDataSection("overwrite", "true"), new MultipartFormDataSection("_sid", _sid), new MultipartFormFileSection("file", bytes, fileName, "application/octet-stream"), }; using var req = UnityWebRequest.Post(url, form); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) onError?.Invoke($"업로드 실패({fileName}): {req.error}"); else Debug.Log($"[NasPublisher] 업로드 완료: {fileName}"); } // ── 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 ParseSid(string json) { const string key = "\"sid\":\""; int start = json.IndexOf(key, StringComparison.Ordinal); if (start < 0) return null; start += key.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}"; } }