Limyee 电商平台为文件上传提供了一个专用的 REST 端点,然后提供一个上下文标识符,在发出其他 REST 请求时可以引用该标识符来附加或嵌入文件。
设置
要对 Limyee 电商平台发出 REST 请求,您必须使用 API 密钥或 OAuth 客户端进行身份验证。以下示例,我们将使用 API 密钥。现在,我们可以设置一个 REST 令牌,将用于每个请求。有关更多信息,请参阅:使用 API 密钥和用户名对 REST 请求进行身份验证。
string YOUR_API_KEY, YOUR_API_KEY_NAME; string restUserToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{YOUR_API_KEY}:{YOUR_API_KEY_NAME}"));
为了简化下面的示例,下面是一个帮助方法,该方法完成建立请求对象和数据、发起请求以及接收和分析响应等通用 Web 任务:
private dynamic SendPostRequest(HttpWebRequest request, byte[] requestData) { // Set up the request request.Method = "POST"; // Add data to request request.ContentLength = requestData.Length; using (var requestStream = request.GetRequestStream()) { requestStream.Write(requestData, 0, requestData.Length); } // Make request using (var response = (HttpWebResponse)request.GetResponse()) { // Read results var stream = response.GetResponseStream(); using (var reader = new StreamReader(stream)) { // Deserialize response var responseData = reader.ReadToEnd(); var serializer = new JavaScriptSerializer(); return serializer.Deserialize<dynamic>(responseData); } } }
能够从Limyee电商平台生成的 REST响应中读取错误也很有帮助,以便进行故障排除。下面是一个示例方法,它将捕获异常并提取错误(如果有)。首先确认这是否是 Limyee 电商平台在处理请求时生成的内部服务器错误,然后从响应读取并解析错误。
private string InvestigateError(Exception ex) { // Investigate for any further error information var errorResponse = (ex as WebException)?.Response as HttpWebResponse; // Ensure this is an internal error from Limyee and not from another source if (errorResponse == null || errorResponse.StatusCode != HttpStatusCode.InternalServerError) return ex.Message; // Read the response string data; using (var reader = new StreamReader(errorResponse.GetResponseStream())) { data = reader.ReadToEnd(); } // Deserialize the response and check for errors var serializer = new JavaScriptSerializer(); var resp = serializer.Deserialize<dynamic>(data); return string.Join("\r\n", resp?["Errors"] ?? ex.Message); }
上传文件
我们将使用"多文件上传"端点。让我们分解一下所需的参数:
UploadContextId:在整个文件上传过程中使用的标识符。这是用户生成的,需要是唯一的,因此请生成 GUID。
ChunkNumber,TotalChunks:我们需要以块的形式传输文件,以避免请求超时和大小限制。分块只是意味着将文件数据拆分为较小的部分并分别发送每个部分。Limyee 电商平台的 REST 端点将处理多个块,组合回一个块,只要为每个请求指定 UploadContextId、ChunkNumber 和 TotalChunks 即可。
文件名:用作使用 UploadContextId 发出多个上载请求时创建或添加的文件名称。
首先,设置变量,包括生成用作 UploadContextId 的 GUID。我们使用 15MB 作为块大小,以在请求大小和所需请求数之间取得平衡。这里最重要的组件是'file'变量,从中我们可以计算出完整操作需要多少个块。对于此文件,我们使用 HttpPostedFile,例如:通过 ASP.NET 文件上传控件上传的文件。
// Input variables const int MAX_CHUNK_SIZE_BYTES = 15728640; var url = "YOUR_SITE_URL"; var uploadContextId = Guid.NewGuid(); // File variables HttpPostedFile file; var fileName = file.FileName; var fileContentType = file.ContentType; var fileDataStream = file.InputStream; // Calculate chunk information int totalChunks = (int) Math.Ceiling((double) fileDataStream.Length/MAX_CHUNK_SIZE_BYTES);
我们需要遍历每个块并上传它。在此处设置循环。
// Open the file reader to read chunks using (var rdr = new BinaryReader(fileDataStream)) { // Send each chunk in succession for (var currentChunk = 0; currentChunk < totalChunks; currentChunk++) { // Chunk uploading code will go here } }
生成表单数据和其他参数,使用 multipart/form-data 的请求格式。我们需要使用此内容类型而不是默认值(application/x-www-form-urlencoded),因为我们在请求正文中传递文件。边界指定每个参数开始和结束的时间。收集完所有必需的参数后,将每个参数,然后将文件本身添加到请求数据中。
// Open the file reader to read chunks using (var rdr = new BinaryReader(fileDataStream)) { // Send each chunk in succession for (var currentChunk = 0; currentChunk < totalChunks; currentChunk++) { var boundary = Guid.NewGuid().ToString("N"); // Create request var request = (HttpWebRequest)WebRequest.Create(url + "/api/v1/cfs/temporary.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "multipart/form-data; boundary=" + boundary; // Collect necessary request information var formData = new Dictionary<string, string> { {"UploadContextId", uploadContextId.ToString()}, {"FileName", fileName}, {"CurrentChunk", currentChunk.ToString()}, {"TotalChunks", totalChunks.ToString()}, }; // Add data to the multi-part form var requestData = new StringBuilder(); foreach (var item in formData) { requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"{item.Key}\"\r\n\r\n{item.Value}\r\n"); } // Add the file itself requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"\r\nContent-Type: {fileContentType}\r\n\r\n"); // TODO: Combine request data with file chunk // TODO: Make request and read response } }
添加所有其他参数后,从文件中读取块并生成完整的请求二进制数据。然后使用帮助程序方法,发出请求并返回上传的文件 URL,确保处理错误。
// Open the file reader to read chunks using (var rdr = new BinaryReader(fileDataStream)) { // Send each chunk in succession for (var currentChunk = 0; currentChunk < totalChunks; currentChunk++) { var boundary = Guid.NewGuid().ToString("N"); // Create request var request = (HttpWebRequest)WebRequest.Create(url + "/api/v1/cfs/temporary.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "multipart/form-data; boundary=" + boundary; // Collect necessary request information var formData = new Dictionary<string, string> { {"UploadContextId", uploadContextId.ToString()}, {"FileName", fileName}, {"CurrentChunk", currentChunk.ToString()}, {"TotalChunks", totalChunks.ToString()}, }; // Add data to the multi-part form var requestData = new StringBuilder(); foreach (var item in formData) { requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"{item.Key}\"\r\n\r\n{item.Value}\r\n"); } // Add the file itself requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"\r\nContent-Type: {fileContentType}\r\n\r\n"); // Prepare data chunk var startDataBytes = Encoding.UTF8.GetBytes(requestData.ToString()); var chunk = rdr.ReadBytes(MAX_CHUNK_SIZE_BYTES); var endDataBytes = Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n"); // Combine all the request data into one array var bytesToSend = new byte[startDataBytes.Length + chunk.Length + endDataBytes.Length]; startDataBytes.CopyTo(bytesToSend, 0); chunk.CopyTo(bytesToSend, startDataBytes.Length); endDataBytes.CopyTo(bytesToSend, startDataBytes.Length + chunk.Length); try { // Make request and read results var response = SendPostRequest(request, bytesToSend); if (response?["Errors"] != null) { return string.Join("\r\n", response["Errors"]); } fileUrl = response?["UploadedFile"]?["DownloadUrl"]; } catch (Exception ex) { return InvestigateError(ex); } } }
成功传输所有块后,您可以在其他请求中使用上传上下文 ID 和/或 URL。
return fileUrl;
更改用户头像
现在,我们已经获得了对上传文件的引用,我们可以在另一个请求(如用户头像更新)中使用该文件。UploadContextId 使 Limyee 电商平台知道请求中使用了什么文件。
首先,使用必要的头数据创建 Web 请求。
var request = (HttpWebRequest)WebRequest.Create(url + $"/api/v1/users/{userId}/avatar.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "application/x-www-form-urlencoded"; // Extra specification needed on UPDATE requests to Limyee request.Headers.Add("Rest-Method", "PUT");
然后收集必要的参数并生成所需的查询字符串格式。使用您在文件上传请求中生成的相同 UploadContextId。然后将表单数据写入请求流。
var values = new Dictionary<string, string> { {"FileUploadContext", uploadContextId.ToString()}, {"FileName", fileName} };
发送请求,并在需要时从响应中提取数据以供参考。捕获并调查任何错误。
try { // Add data to request var queryString = string.Join("&", values.Select(v => $"{v.Key}={v.Value}")); var bytesToSend = Encoding.UTF8.GetBytes(queryString); // Send request and check response var response = SendPostRequest(request, bytesToSend); return response?["AvatarUrl"]; } catch (Exception ex) { return InvestigateError(ex); }
如果请求成功,则刷新网站后,新的头像图像立即可见。
在帖子中嵌入文件
我们还可以将上传的文件嵌入到任何支持在正文中嵌入文件的内容中。在此示例中,我们将使用博客文章创建端点。使用标准嵌入语法,该语法要求上传响应可以生成并返回文件 url。
[View:FILE_URL:WIDTH:HEIGHT]
如上所述,使用必要的头数据创建 Web 请求。
var request = (HttpWebRequest)WebRequest.Create(url + $"/api/v1/blogs/{blogId}/posts.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "application/x-www-form-urlencoded";
然后收集必要的参数并生成所需的查询字符串。使用您在文件上传请求中生成的相同 UploadContextId。然后将表单数据写入请求流。
var values = new Dictionary<string, string> { {"Title", $"POST_TITLE {Guid.NewGuid()}"}, {"Body", $"POST_TEXT [View:{fileUrl}:300:300] POST_TEXT"} };
发送请求,并在需要时从响应中提取数据以供参考。捕获并调查任何错误。
try { // Add data to request var queryString = string.Join("&", values.Select(v => $"{v.Key}={v.Value}")); var bytesToSend = Encoding.UTF8.GetBytes(queryString); // Send request and check response var response = SendPostRequest(request, bytesToSend); return response?["BlogPost"]?["Url"]; } catch (Exception ex) { return InvestigateError(ex); }
如果请求成功,则带有嵌入图像的博客文章现在应该在您的 Limyee 电商平台网站上显示。
清理临时文件
Limyee 电商平台最终会通过删除 id 来清理旧的文件,这意味着文件将保留在站点上使用的任何位置,但上下文 ID 在将来的请求中不再可用。此清理的时间可以在 MultipleFileUploadCleanupJob 中配置。
完整示例
以下是此示例中使用的所有代码以及用于测试的ASP.NET 窗体。
FileUpload.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FileUpload.aspx.cs" Inherits="SampleRESTFileUpload.FileUpload" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div> <asp:FileUpload runat="server" id="fuFile"/> <br/> <asp:TextBox runat="server" id="tbUrl"></asp:TextBox> <br/> <asp:TextBox runat="server" id="tbApiKeyName"></asp:TextBox> <br/> <asp:TextBox runat="server" id="tbApiKey"></asp:TextBox> <br/> <asp:Button runat="server" id="btnSubmit" Text="Submit"/> <br/> <asp:Label runat="server" id="lblInfo"></asp:Label> </div> </form> </body> </html>
FileUpload.aspx.cs
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Web; using System.Web.Script.Serialization; namespace SampleRESTFileUpload { public partial class FileUpload : System.Web.UI.Page { protected System.Web.UI.HtmlControls.HtmlForm form1; protected System.Web.UI.WebControls.FileUpload fuFile; protected System.Web.UI.WebControls.TextBox tbUrl; protected System.Web.UI.WebControls.TextBox tbApiKeyName; protected System.Web.UI.WebControls.TextBox tbApiKey; protected System.Web.UI.WebControls.Button btnSubmit; protected System.Web.UI.WebControls.Label lblInfo; protected void Page_Load(object sender, EventArgs e) { btnSubmit.Click += BtnSubmit_Click; } private void BtnSubmit_Click(object sender, EventArgs e) { var file = fuFile.PostedFile; var url = tbUrl.Text; // Calculate REST token var apiKey = tbApiKey.Text; var apiKeyName = tbApiKeyName.Text; var restUserToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{apiKeyName}")); var uploadContextId = Guid.NewGuid(); var uploadUrl = UploadFile(restUserToken, url, uploadContextId, file); var avatarUrl = ChangeAvatar(restUserToken, url, file.FileName, uploadContextId, 2100); var postUrl = CreatePost(restUserToken, url, uploadUrl, 1); lblInfo.Text = $"{uploadContextId}<br />{uploadUrl}<br />{avatarUrl}<br />{postUrl}"; } private const int MAX_CHUNK_SIZE_BYTES = 15728640; public string UploadFile(string restUserToken, string url, Guid uploadContextId, HttpPostedFile file) { string fileUrl = null; // Name file variables var fileName = file.FileName; var fileContentType = file.ContentType; var fileDataStream = file.InputStream; // Calculate chunk information int totalChunks = (int) Math.Ceiling((double) fileDataStream.Length/MAX_CHUNK_SIZE_BYTES); // Open the file reader to read chunks using (var rdr = new BinaryReader(fileDataStream)) { // Send each chunk in succession for (var currentChunk = 0; currentChunk < totalChunks; currentChunk++) { var boundary = Guid.NewGuid().ToString("N"); // Create request var request = (HttpWebRequest)WebRequest.Create(url + "/api/v1/cfs/temporary.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "multipart/form-data; boundary=" + boundary; // Collect necessary request information var formData = new Dictionary<string, string> { {"UploadContextId", uploadContextId.ToString()}, {"FileName", fileName}, {"CurrentChunk", currentChunk.ToString()}, {"TotalChunks", totalChunks.ToString()}, }; // Add data to the multi-part form var requestData = new StringBuilder(); foreach (var item in formData) { requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"{item.Key}\"\r\n\r\n{item.Value}\r\n"); } // Add the file itself requestData.Append($"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"\r\nContent-Type: {fileContentType}\r\n\r\n"); // Prepare data chunk var startDataBytes = Encoding.UTF8.GetBytes(requestData.ToString()); var chunk = rdr.ReadBytes(MAX_CHUNK_SIZE_BYTES); var endDataBytes = Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n"); // Combine all the request data into one array var bytesToSend = new byte[startDataBytes.Length + chunk.Length + endDataBytes.Length]; startDataBytes.CopyTo(bytesToSend, 0); chunk.CopyTo(bytesToSend, startDataBytes.Length); endDataBytes.CopyTo(bytesToSend, startDataBytes.Length + chunk.Length); try { // Make request and read results var response = SendPostRequest(request, bytesToSend); if (response?["Errors"] != null) { return string.Join("\r\n", response["Errors"]); } fileUrl = response?["UploadedFile"]?["DownloadUrl"]; } catch (Exception ex) { return InvestigateError(ex); } } } return fileUrl; } public string ChangeAvatar(string restUserToken, string url, string fileName, Guid uploadContextId, int userId) { // Create request var request = (HttpWebRequest)WebRequest.Create(url + $"/api/v1/users/{userId}/avatar.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "application/x-www-form-urlencoded"; // Extra specification needed on UPDATE requests to Limyee request.Headers.Add("Rest-Method", "PUT"); // Collect necessary request information var values = new Dictionary<string, string> { {"FileUploadContext", uploadContextId.ToString()}, {"FileName", fileName} }; try { // Add data to request var queryString = string.Join("&", values.Select(v => $"{v.Key}={v.Value}")); var bytesToSend = Encoding.UTF8.GetBytes(queryString); // Send request and check response var response = SendPostRequest(request, bytesToSend); return response?["AvatarUrl"]; } catch (Exception ex) { return InvestigateError(ex); } } public string CreatePost(string restUserToken, string url, string fileUrl, int blogId) { // Create request var request = (HttpWebRequest)WebRequest.Create(url + $"/api/v1/blogs/{blogId}/posts.json"); request.Headers.Add("Rest-User-Token", restUserToken); request.ContentType = "application/x-www-form-urlencoded"; // Collect necessary request information var values = new Dictionary<string, string> { {"Title", $"POST_TITLE {Guid.NewGuid()}"}, {"Body", $"POST_TEXT [View:{fileUrl}:300:300] POST_TEXT"} }; try { // Add data to request var queryString = string.Join("&", values.Select(v => $"{v.Key}={v.Value}")); var bytesToSend = Encoding.UTF8.GetBytes(queryString); // Send request and check response var response = SendPostRequest(request, bytesToSend); return response?["BlogPost"]?["Url"]; } catch (Exception ex) { return InvestigateError(ex); } } private dynamic SendPostRequest(HttpWebRequest request, byte[] requestData) { // Set up the request request.Method = "POST"; // Add data to request request.ContentLength = requestData.Length; using (var requestStream = request.GetRequestStream()) { requestStream.Write(requestData, 0, requestData.Length); } // Make request using (var response = (HttpWebResponse)request.GetResponse()) { // Read results var stream = response.GetResponseStream(); using (var reader = new StreamReader(stream)) { // Deserialize response var responseData = reader.ReadToEnd(); var serializer = new JavaScriptSerializer(); return serializer.Deserialize<dynamic>(responseData); } } } private string InvestigateError(Exception ex) { // Investigate for any further error information var errorResponse = (ex as WebException)?.Response as HttpWebResponse; // Ensure this is an internal error from Limyee and not from another source if (errorResponse == null || errorResponse.StatusCode != HttpStatusCode.InternalServerError) return ex.Message; // Read the response string data; using (var reader = new StreamReader(errorResponse.GetResponseStream())) { data = reader.ReadToEnd(); } // Deserialize the response and check for errors var serializer = new JavaScriptSerializer(); var resp = serializer.Deserialize<dynamic>(data); return string.Join("\r\n", resp?["Errors"] ?? ex.Message); } } }