LOADING

加载过慢请开启缓存 浏览器默认开启

unity实现cloudflarer2存储管理

2025/12/10 unity unity

unity实现cloudflarer2存储管理

实现效果:

R2UnityWebClient.cs:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class R2UnityWebClient : MonoBehaviour
{
    //界面
    InputField _InputListFolerName;
    Button _BtnListFiles;
    Text _LabFiles;
    StringBuilder _SbFiles=new StringBuilder();
    InputField _InputFolerName;
    Button _BtnCreateFolder;
    InputField _InputKey;
    Button _BtnDel;
    InputField _InputBucket;
    Button _BtnChange;
    Text _LabBucket;

    [Header("R2 配置")]
    public string accountId = "accountId";
    public string apiToken = "apiToken";
    public string bucketName = "defaultbucket";

    private string _apiBaseUrl;
    private void Awake()
    {
        _InputListFolerName=transform.Find("InputListFolerName").GetComponent<InputField>();
        _BtnListFiles = transform.Find("BtnListFiles").GetComponent<Button>();
        _LabFiles = transform.Find("LabFiles").GetComponent<Text>();
        _InputFolerName = transform.Find("InputFolerName").GetComponent<InputField>();
        _BtnCreateFolder = transform.Find("BtnCreateFolder").GetComponent<Button>();
        _InputKey = transform.Find("InputKey").GetComponent<InputField>();
        _BtnDel = transform.Find("BtnDel").GetComponent<Button>();
        _InputBucket = transform.Find("InputBucket").GetComponent<InputField>();
        _BtnChange = transform.Find("BtnChange").GetComponent<Button>();
        _LabBucket = transform.Find("LabBucket").GetComponent<Text>();
    }
   
    void Start()
    {
        _BtnListFiles.onClick.AddListener(ListRootContents);
        _BtnChange.onClick.AddListener(ChangeBucket);
        _BtnCreateFolder.onClick.AddListener(CreateFolder);
        _BtnDel.onClick.AddListener(DelFile);
        // 初始化API基础URL
        UpdateAPI();
        UpdateBucket();
        Debug.Log($"R2客户端初始化完成,基础URL: {_apiBaseUrl}");
    }
    void DelFile()
    {
        if (string.IsNullOrWhiteSpace(_InputKey.text))
        {
            Debug.LogError("禁止直接删除根目录");
            return;
        }
        DeleteFile(_InputKey.text, (isFinsh, key) =>
        {
            if (isFinsh)
            {
                _InputKey.text = "";
                Debug.Log("文件删除完成:" + key);
            }
            else
            {
                Debug.Log("文件删除失败:" + key);
            }
        });
    }
    void CreateFolder()
    {
        CreateFolder(_InputFolerName.text, (isFinsh, key) =>
        {
            if (isFinsh)
            {
                _InputFolerName.text = "";
                Debug.Log("文件夹创建完成:" + key);
            }
            else
            {
                Debug.Log("文件夹创建失败:" + key);
            }
        });
    }
    void UpdateAPI()
    {
        _apiBaseUrl = $"https://api.cloudflare.com/client/v4/accounts/{accountId}/r2/buckets/{bucketName}";
    }
    void ChangeBucket()
    {
        bucketName = _InputBucket.text;
        UpdateAPI();
        UpdateBucket();
    }
    void UpdateBucket()
    {
        _LabBucket.text = "当前存储桶:" + bucketName;
    }
    void ListRootContents()
    {
        _SbFiles.Clear();
        _SbFiles.AppendLine("当前文件夹:" + _InputListFolerName.text);
        Debug.Log("正在列出"+ _InputListFolerName.text + "内容...");

        ListBucketContents(_InputListFolerName.text,false, (success, files, folders) =>
        {
            if (success)
            {
                Debug.Log($"找到 {files.Count} 个文件,{folders.Count} 个文件夹");

                foreach (var folder in folders)
                {
                    _SbFiles.AppendLine("文件夹:"+folder);
                }

                foreach (var file in files)
                {
                    _SbFiles.AppendLine("文件:" + file.key+" 文件大小:"+file.GetFormattedSize());
                }
            }
            else
            {
                Debug.LogError("列出内容失败");
            }
            _LabFiles.text= _SbFiles.ToString();
        });
    }
    /// <summary>
    /// 通用请求协程,处理所有R2 API调用
    /// </summary>
    private IEnumerator SendRequestCoroutine(UnityWebRequest request, Action<bool, string, byte[]> callback)
    {
        // 设置认证头
        request.SetRequestHeader("Authorization", $"Bearer {apiToken}");
        request.SetRequestHeader("Content-Type", "application/json");

        // 发送请求
        yield return request.SendWebRequest();

        bool success = false;
        string error = "";
        byte[] data = null;

        // 检查结果
        if (request.result == UnityWebRequest.Result.Success)
        {
            success = true;
            data = request.downloadHandler.data;
            Debug.Log($"请求成功: {request.url}");
        }
        else
        {
            error = $"请求失败 ({request.result}): {request.error}";
            if (request.downloadHandler != null && !string.IsNullOrEmpty(request.downloadHandler.text))
            {
                error += $"\n响应: {request.downloadHandler.text}";
            }
            Debug.LogError(error);
        }

        // 调用回调
        callback?.Invoke(success, error, data);

        // 清理请求
        request.Dispose();
    }

    // ========== 核心功能实现 ==========

    /// <summary>
    /// 1. 上传文件到R2
    /// </summary>
    public void UploadFile(string objectKey, byte[] fileData, string contentType, Action<bool, string> callback)
    {
        StartCoroutine(UploadFileCoroutine(objectKey, fileData, contentType, callback));
    }

    private IEnumerator UploadFileCoroutine(string objectKey, byte[] fileData, string contentType, Action<bool, string> callback)
    {
        string url = $"{_apiBaseUrl}/objects/{Uri.EscapeDataString(objectKey)}";

        UnityWebRequest request = new UnityWebRequest(url, "PUT");
        request.uploadHandler = new UploadHandlerRaw(fileData);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", contentType);

        yield return StartCoroutine(SendRequestCoroutine(request, (success, error, data) =>
        {
            if (success)
            {
                Debug.Log($"文件上传成功: {objectKey}");
                callback?.Invoke(true, "上传成功");
            }
            else
            {
                callback?.Invoke(false, $"上传失败: {error}");
            }
        }));
    }

    ///// <summary>
    ///// 2. 列出文件和文件夹
    ///// </summary>
    public void ListBucketContents(string prefix, bool includeFolders = true,
                                   Action<bool, List<FileDetail>, List<string>> callback = null)
    {
        StartCoroutine(ListBucketContentsCoroutine(prefix, includeFolders, callback));
    }

    private IEnumerator ListBucketContentsCoroutine(string prefix, bool includeFolders,
                                                    Action<bool, List<FileDetail>, List<string>> callback)
    {
        // 构建查询
        string query = includeFolders ? "?delimiter=/" : "";
        if (!string.IsNullOrEmpty(prefix))
        {
            query += (query.Length > 0 ? "&" : "?") + $"prefix={Uri.EscapeDataString(prefix)}";
        }

        string url = $"{_apiBaseUrl}/objects{query}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        yield return StartCoroutine(SendRequestCoroutine(request, (success, error, data) =>
        {
            List<FileDetail> fileDetails = new List<FileDetail>();
            List<string> folderPaths = new List<string>();

            if (success && data != null)
            {
                string json = Encoding.UTF8.GetString(data);
                try
                {
                    var response = JsonUtility.FromJson<R2ApiResponse>(json);

                    if (response != null && response.success && response.result != null)
                    {
                        foreach (var obj in response.result)
                        {
                            var detail = new FileDetail
                            {
                                key = obj.key,
                                size = obj.size,
                                lastModified = obj.last_modified,
                                contentType = obj.http_metadata?.contentType ?? "未知"
                            };

                            // 判断是文件还是文件夹
                            if (obj.key.EndsWith("/") && obj.size == 0)
                            {
                                folderPaths.Add(obj.key);
                            }
                            else
                            {
                                fileDetails.Add(detail);
                            }
                        }

                        Debug.Log($"成功列出: {fileDetails.Count} 个文件, {folderPaths.Count} 个文件夹");
                        callback?.Invoke(true, fileDetails, folderPaths);
                        return;
                    }
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"第一种解析方式失败,尝试备用方案: {e.Message}");
                }

                // 备用方案:手动解析
                try
                {
                    // 简单的字符串查找(应急方案)
                    int startPos = json.IndexOf("\"result\":[");
                    if (startPos > 0)
                    {
                        // ... 这里可以添加简单的手动解析逻辑
                    }
                }
                catch (Exception e)
                {
                    Debug.LogError($"所有解析方式都失败: {e.Message}");
                }
            }

            callback?.Invoke(false, fileDetails, folderPaths);
        }));
    }

    // 文件详情类
    [System.Serializable]
    public class FileDetail
    {
        public string key;
        public long size;
        public string lastModified;
        public string contentType;

        public string GetFormattedSize()
        {
            if (size < 1024) return $"{size} B";
            else if (size < 1024 * 1024) return $"{(size / 1024.0):F1} KB";
            else if (size < 1024 * 1024 * 1024) return $"{(size / (1024.0 * 1024.0)):F1} MB";
            else return $"{(size / (1024.0 * 1024.0 * 1024.0)):F2} GB";
        }
    }
    /// <summary>
    /// 3. 删除文件
    /// </summary>
    public void DeleteFile(string objectKey, Action<bool, string> callback)
    {
        StartCoroutine(DeleteFileCoroutine(objectKey, callback));
    }

    private IEnumerator DeleteFileCoroutine(string objectKey, Action<bool, string> callback)
    {
        string url = $"{_apiBaseUrl}/objects/{Uri.EscapeDataString(objectKey)}";
        UnityWebRequest request = UnityWebRequest.Delete(url);

        yield return StartCoroutine(SendRequestCoroutine(request, (success, error, data) =>
        {
            if (success)
            {
                Debug.Log($"文件删除成功: {objectKey}");
                callback?.Invoke(true, "删除成功");
            }
            else
            {
                callback?.Invoke(false, $"删除失败: {error}");
            }
        }));
    }

    /// <summary>
    /// 4. 移动/重命名文件
    /// </summary>
    public void MoveFile(string sourceKey, string destinationKey, Action<bool, string> callback)
    {
        StartCoroutine(MoveFileCoroutine(sourceKey, destinationKey, callback));
    }

    private IEnumerator MoveFileCoroutine(string sourceKey, string destinationKey, Action<bool, string> callback)
    {
        // 第一步:复制文件
        string copyUrl = $"{_apiBaseUrl}/objects/{Uri.EscapeDataString(sourceKey)}/copy";

        var copyBody = new CopyRequest { destination_key = destinationKey };
        string jsonBody = JsonUtility.ToJson(copyBody);

        UnityWebRequest copyRequest = new UnityWebRequest(copyUrl, "POST");
        byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
        copyRequest.uploadHandler = new UploadHandlerRaw(bodyRaw);
        copyRequest.downloadHandler = new DownloadHandlerBuffer();
        copyRequest.SetRequestHeader("Content-Type", "application/json");

        bool copySuccess = false;

        yield return StartCoroutine(SendRequestCoroutine(copyRequest, (success, error, data) =>
        {
            copySuccess = success;
            if (!success)
            {
                callback?.Invoke(false, $"复制失败: {error}");
            }
        }));

        if (!copySuccess) yield break;

        // 第二步:删除原文件
        yield return StartCoroutine(DeleteFileCoroutine(sourceKey, (deleteSuccess, deleteMessage) =>
        {
            if (deleteSuccess)
            {
                Debug.Log($"文件移动成功: {sourceKey} -> {destinationKey}");
                callback?.Invoke(true, "移动成功");
            }
            else
            {
                callback?.Invoke(false, $"移动失败(删除原文件时出错): {deleteMessage}");
            }
        }));
    }

    /// <summary>
    /// 5. 创建文件夹(上传一个空对象)
    /// </summary>
    public void CreateFolder(string folderPath, Action<bool, string> callback)
    {
        // 确保文件夹路径以斜杠结尾
        if (!folderPath.EndsWith("/"))
        {
            folderPath += "/";
        }

        // 上传空内容作为文件夹标记
        byte[] emptyData = new byte[0];
        UploadFile(folderPath, emptyData, "application/x-directory", callback);
    }

    /// <summary>
    /// 6. 下载文件
    /// </summary>
    public void DownloadFile(string objectKey, Action<bool, byte[], string> callback)
    {
        StartCoroutine(DownloadFileCoroutine(objectKey, callback));
    }

    private IEnumerator DownloadFileCoroutine(string objectKey, Action<bool, byte[], string> callback)
    {
        string url = $"{_apiBaseUrl}/objects/{Uri.EscapeDataString(objectKey)}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        yield return StartCoroutine(SendRequestCoroutine(request, (success, error, data) =>
        {
            if (success && data != null)
            {
                callback?.Invoke(true, data, "下载成功");
            }
            else
            {
                callback?.Invoke(false, null, $"下载失败: {error}");
            }
        }));
    }

    /// <summary>
    /// 7. 获取文件信息(大小、修改时间等)
    /// </summary>
    public void GetFileInfo(string objectKey, Action<bool, FileInfo> callback)
    {
        StartCoroutine(GetFileInfoCoroutine(objectKey, callback));
    }

    private IEnumerator GetFileInfoCoroutine(string objectKey, Action<bool, FileInfo> callback)
    {
        string url = $"{_apiBaseUrl}/objects/{Uri.EscapeDataString(objectKey)}";
        UnityWebRequest request = UnityWebRequest.Head(url); // 使用HEAD请求只获取元数据

        yield return StartCoroutine(SendRequestCoroutine(request, (success, error, data) =>
        {
            if (success)
            {
                FileInfo info = new FileInfo
                {
                    key = objectKey,
                    size = long.Parse(request.GetResponseHeader("Content-Length") ?? "0"),
                    lastModified = request.GetResponseHeader("Last-Modified"),
                    contentType = request.GetResponseHeader("Content-Type")
                };

                callback?.Invoke(true, info);
            }
            else
            {
                callback?.Invoke(false, null);
            }
        }));
    }
}

// ========== 数据模型类 ==========

// 替换掉你原来的 R2ListResponse, ListResult 等类
[System.Serializable]
public class R2ApiResponse
{
    public bool success;
    public List<R2Object> result; // 注意:这里直接就是对象列表,不是嵌套结构
    public List<object> errors;
    public List<object> messages;
}

[System.Serializable]
public class R2Object
{
    public string key;
    public long size;
    public string last_modified; // 注意API返回的是下划线格式
    public string etag;
    public HttpMetadata http_metadata;
    public Dictionary<string, string> custom_metadata;
    public string storage_class;
}

[System.Serializable]
public class HttpMetadata
{
    public string contentType;
    // 可能还有其他HTTP元数据字段
}

// 注意:API在添加 delimiter=“/” 参数时,才会返回 common_prefixes 来表示文件夹
// 这种情况的响应结构不同,需要单独处理
[System.Serializable]
public class R2ListWithPrefixesResponse
{
    public bool success;
    public ListWithPrefixesResult result;
    public List<object> errors;
    public List<object> messages;
}

[System.Serializable]
public class ListWithPrefixesResult
{
    public List<R2Object> objects; // 或者可能是 contents
    public List<CommonPrefix> common_prefixes;
}

[System.Serializable]
public class CommonPrefix
{
    public string prefix;
}

[System.Serializable]
public class CopyRequest
{
    public string destination_key;
}

[System.Serializable]
public class FileInfo
{
    public string key;
    public long size;
    public string lastModified;
    public string contentType;
}

下载Demo:
https://photo.lovekeli.uk/blog/cloudflareR2.unitypackage