В своем последнем посте я говорил о функции подписи общего доступа в Windows Azure Storage. Вы можете прочитать этот пост здесь: http://gauravmantri.com/2013/02/13/revisiting-windows-azure-shared-access-signature/ . В этом посте мы рассмотрим это на практике .
Одна вещь, которую я хотел сделать недавно — это возможность загружать очень большие файлы в хранилище BLOB-объектов Windows Azure из веб-приложения . Общий подход заключается в чтении файла через веб-приложение с помощью « Файл”HTML контролирует и загружает весь этот файл в некоторый код на стороне сервера, который затем загружает файл в хранилище BLOB-объектов. Этот подход будет работать лучше для небольших файлов, но он будет ужасно терпеть неудачу, когда дело касается файлов от умеренного до очень большого размера, так как элемент управления загрузкой файлов будет загружать весь файл на сервер (для больших файлов это приведет к тайм-ауту в зависимости от вашего интернет-соединения) и затем этот файл находится в памяти сервера, прежде чем можно будет предпринять какие-либо действия с этим файлом (опять же, для больших файлов это может вызвать проблемы с производительностью, если тысячи пользователей загружают тысячи больших файлов).
В этом посте мы поговорим о том, как этого добиться, используя функцию File API в HTML5 .
Пожалуйста, обратите внимание, что я не эксперт по JavaScript / HTML / CSS . Около 4–5 лет назад я много занимался разработкой JavaScript и создавал некоторые действительно безумные приложения с использованием JavaScript (например, функции слияния почты, автоматизация делопроизводства, взаимодействие с нативными устройствами с использованием JS), но это было тогда. Последние 4-5 лет я интенсивно занимаюсь разработкой приложений для настольных компьютеров. Таким образом, большая часть кода JS, который вы увидите ниже, скопирована с различных сайтов и StackOverflow .
Цели
Следующее было некоторыми из целей, которые я имел в виду:
- Интерфейс должен быть веб-ориентированным (возможно, чистый HTML).
- Мне не нужно предоставлять учетные данные своей учетной записи хранения конечным пользователям.
- Если возможно, не должно быть никакого кода на стороне сервера (ASP.Net/PHP и т. Д.), Т. Е. Это должен быть чистый HTML, а связь должна осуществляться между веб-браузером пользователя и учетной записью хранилища.
- Если возможно, решение должно загружать файл порциями, чтобы можно было загружать большие файлы, не считывая их полностью за один раз.
Теперь посмотрим, как мы можем достичь наших целей.
Решение
Вот некоторые из вещей, которые я сделал для достижения целей, перечисленных выше.
Chunking File
Первая проблема, с которой я столкнулся, заключается в том, как загрузить файл порциями. Все примеры, которые я видел, загружали весь файл, но это не то, что я хотел. Наконец, я натолкнулся на этот пример на HTML5 Rocks, где они говорили о File API : http://www.html5rocks.com/en/tutorials/file/dndfiles/ . В основном, что меня заинтересовало, так это функция « нарезки », доступная в файловом интерфейсе HTML 5 . Короче говоря, эта функция слайса состоит в том, что она асинхронно читает часть файла и возвращает эти данные . Это именно то, на что я смотрел. Когда я узнал об этом, я понял, что 75% моей работы выполнено! Все остальное было бризом
Защита учетных данных учетной записи хранения
Следующей вещью в моем списке была защита учетных данных учетной записи хранения, и это было довольно безболезненно, поскольку я точно знал, что мне нужно было сделать — подпись общего доступа . SAS предоставил мне защищенный URI, с помощью которого пользователи смогут загружать файлы в мою учетную запись хранения, а я не предоставляю им доступ к учетным данным учетной записи хранения. Это было подробно освещено в моем посте о том же самом: Пересмотр подписи общего доступа Windows Azure .
Прямая связь между клиентским приложением и хранилищем Windows Azure
Следующим шагом было облегчить прямую связь между клиентским приложением и хранилищем. Как мы знаем, хранилище Windows Azure построено на REST, поэтому я могу просто использовать функциональность AJAX для взаимодействия с REST API. Во время написания этого блога важно понимать, что Windows Azure Storage по-прежнему не поддерживает перекрестный общий доступ к ресурсам (CORS) . Это означает, что ваше веб-приложение и хранилище BLOB-объектов должны находиться в одном домене. Решением этой проблемы является размещение вашего HTML-приложения в общедоступном контейнере больших двоичных объектов в той же учетной записи хранения, куда вы хотите, чтобы пользователи загружали файлы., Мне сказали, что в ближайшее время поддержка CORS появится в хранилище Windows Azure, и как только это произойдет, вам не нужно размещать это приложение в этой учетной записи хранения, но до этого вам придется мириться с этим ограничением.
Код
Теперь давайте посмотрим на код.
HTML интерфейс
Поскольку я пытался взломать код, я оставил интерфейс довольно простым. Вот как выглядит мой HTML-код:
<body> <form> <div style="margin-left: 20px;"> <h1>File Uploader</h1> <p> <strong>SAS URI</strong>: <br/> <span class="input-control text"> <input type="text" id="sasUrl" style="width: 50%" value=""/> </span> </p> <p> <strong>File To Upload</strong>: <br/> <span class="input-control text"> <input type="file" id="file" name="file" style="width: 50%"/> </span> </p> <div id="output"> <strong>File Properties:</strong> <br/> <p> Name: <span id="fileName"></span> </p> <p> File Size: <span id="fileSize"></span> bytes. </p> <p> File Type: <span id="fileType"></span> </p> <p> <input type="button" value="Upload File" onclick="uploadFileInBlocks()"/> </p> <p> <strong>Progress</strong>: <span id="fileUploadProgress">0.00 %</span> </p> </div> </div> <div> </div> </form> </body>
Все это имеет текстовое поле для пользователя, чтобы ввести SAS URI и HTML-файл управления. Когда пользователь выбирает файл, я отображаю свойства файла и кнопку «Загрузить», чтобы начать загрузку файла.
Чтение свойств файла
Для отображения свойств файла я использовал событие « onchange » элемента File . Событие дало мне список файлов. Поскольку я загружал только один файл, я выбрал первый файл из этого списка и получил его имя (у blob было бы это имя), размер (размер BLOB-объекта и определение размера фрагмента) и тип (для установки свойства типа содержимого BLOB-объекта).
//Bind the change event. $("#file").bind('change', handleFileSelect); //Read the file and find out how many blocks we would need to split it. function handleFileSelect(e) { var files = e.target.files; selectedFile = files[0]; $("#fileName").text(selectedFile.name); $("#fileSize").text(selectedFile.size); $("#fileType").text(selectedFile.type); }
лязг
В моем приложении я сделал предположение, что разделю файл на куски размером 256 КБ. Как только я нашел размер файла, я просто узнал общее количество кусков.
//Read the file and find out how many blocks we would need to split it. function handleFileSelect(e) { maxBlockSize = 256 * 1024; currentFilePointer = 0; totalBytesRemaining = 0; var files = e.target.files; selectedFile = files[0]; $("#output").show(); $("#fileName").text(selectedFile.name); $("#fileSize").text(selectedFile.size); $("#fileType").text(selectedFile.type); var fileSize = selectedFile.size; if (fileSize < maxBlockSize) { maxBlockSize = fileSize; console.log("max block size = " + maxBlockSize); } totalBytesRemaining = fileSize; if (fileSize % maxBlockSize == 0) { numberOfBlocks = fileSize / maxBlockSize; } else { numberOfBlocks = parseInt(fileSize / maxBlockSize, 10) + 1; } console.log("total blocks = " + numberOfBlocks); }
Конечная точка для загрузки файла
URI SAS фактически представляет URI для контейнера BLOB-объектов. Поскольку мне нужно было создать конечную точку для загрузки файла, я разделил URI (путь и запрос) и добавил имя файла к пути, а затем снова добавил запрос в конец.
var baseUrl = $("#sasUrl").val(); var indexOfQueryStart = baseUrl.indexOf("?"); submitUri = baseUrl.substring(0, indexOfQueryStart) + '/' + selectedFile.name + baseUrl.substring(indexOfQueryStart); console.log(submitUri);
Чтение Чанка
Вот где должна появиться функция chunk File API. В коде происходит следующее: когда пользователь нажимает кнопку загрузки, я асинхронно считываю часть этого файла и получаю байтовый массив. Этот байтовый массив будет загружен.
var reader = new FileReader(); var fileContent = selectedFile.slice(currentFilePointer, currentFilePointer + maxBlockSize); reader.readAsArrayBuffer(fileContent);
Загрузка чанка
Поскольку я хотел реализовать загрузку в чанке, как только чтение будет прочитано из файла, я создаю запрос Put Block на основе спецификации API Put Block REST, используя функцию JJuery AJAX, и передаю этот чанк как данные. Как только этот запрос успешно завершен, я читаю следующий блок и повторяю процесс до тех пор, пока все фрагменты не будут обработаны.
reader.onloadend = function (evt) { if (evt.target.readyState == FileReader.DONE) { // DONE == 2 var uri = submitUri + '&comp=block&blockid=' + blockIds[blockIds.length - 1]; var requestData = new Uint8Array(evt.target.result); $.ajax({ url: uri, type: "PUT", data: requestData, processData: false, beforeSend: function(xhr) { xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); xhr.setRequestHeader('Content-Length', requestData.length); }, success: function (data, status) { console.log(data); console.log(status); bytesUploaded += requestData.length; var percentComplete = ((parseFloat(bytesUploaded) / parseFloat(selectedFile.size)) * 100).toFixed(2); $("#fileUploadProgress").text(percentComplete + " %"); uploadFileInBlocks(); }, error: function(xhr, desc, err) { console.log(desc); console.log(err); } }); } };
Передача Blob
Последним шагом в этом процессе является фиксация большого двоичного объекта в хранилище больших двоичных объектов. Для этого я создаю Поместить Block List запрос , основанный на Put API блоков REST спецификации и процесс этот запрос еще раз с помощью функции AJAX JQuery и передать список блоков в качестве данных. Это завершило процесс.
function commitBlockList() { var uri = submitUri + '&comp=blocklist'; console.log(uri); var requestBody = '<?xml version="1.0" encoding="utf-8"?><BlockList>'; for (var i = 0; i < blockIds.length; i++) { requestBody += '<Latest>' + blockIds[i] + '</Latest>'; } requestBody += '</BlockList>'; console.log(requestBody); $.ajax({ url: uri, type: "PUT", data: requestBody, beforeSend: function (xhr) { xhr.setRequestHeader('x-ms-blob-content-type', selectedFile.type); xhr.setRequestHeader('Content-Length', requestBody.length); }, success: function (data, status) { console.log(data); console.log(status); }, error: function (xhr, desc, err) { console.log(desc); console.log(err); } });
Полный код
Вот полный код. Для CSS я фактически использовал Metro UI CSS — http://metroui.org.ua/ . Если вы планируете создавать веб-приложения и хотите придать им стиль приложений Windows 8, попробуйте. Это довольно круто + это с открытым исходным кодом. На самом деле нет причин для вас, чтобы не попробовать!
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>File Uploader</title> <script src="js/jquery-1.7.1.js"></script> <link rel="stylesheet" href="css/modern.css"/> <script> var maxBlockSize = 256 * 1024;//Each file will be split in 256 KB. var numberOfBlocks = 1; var selectedFile = null; var currentFilePointer = 0; var totalBytesRemaining = 0; var blockIds = new Array(); var blockIdPrefix = "block-"; var submitUri = null; var bytesUploaded = 0; $(document).ready(function () { $("#output").hide(); $("#file").bind('change', handleFileSelect); if (window.File && window.FileReader && window.FileList && window.Blob) { // Great success! All the File APIs are supported. } else { alert('The File APIs are not fully supported in this browser.'); } }); //Read the file and find out how many blocks we would need to split it. function handleFileSelect(e) { maxBlockSize = 256 * 1024; currentFilePointer = 0; totalBytesRemaining = 0; var files = e.target.files; selectedFile = files[0]; $("#output").show(); $("#fileName").text(selectedFile.name); $("#fileSize").text(selectedFile.size); $("#fileType").text(selectedFile.type); var fileSize = selectedFile.size; if (fileSize < maxBlockSize) { maxBlockSize = fileSize; console.log("max block size = " + maxBlockSize); } totalBytesRemaining = fileSize; if (fileSize % maxBlockSize == 0) { numberOfBlocks = fileSize / maxBlockSize; } else { numberOfBlocks = parseInt(fileSize / maxBlockSize, 10) + 1; } console.log("total blocks = " + numberOfBlocks); var baseUrl = $("#sasUrl").val(); var indexOfQueryStart = baseUrl.indexOf("?"); submitUri = baseUrl.substring(0, indexOfQueryStart) + '/' + selectedFile.name + baseUrl.substring(indexOfQueryStart); console.log(submitUri); } var reader = new FileReader(); reader.onloadend = function (evt) { if (evt.target.readyState == FileReader.DONE) { // DONE == 2 var uri = submitUri + '&comp=block&blockid=' + blockIds[blockIds.length - 1]; var requestData = new Uint8Array(evt.target.result); $.ajax({ url: uri, type: "PUT", data: requestData, processData: false, beforeSend: function(xhr) { xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); xhr.setRequestHeader('Content-Length', requestData.length); }, success: function (data, status) { console.log(data); console.log(status); bytesUploaded += requestData.length; var percentComplete = ((parseFloat(bytesUploaded) / parseFloat(selectedFile.size)) * 100).toFixed(2); $("#fileUploadProgress").text(percentComplete + " %"); uploadFileInBlocks(); }, error: function(xhr, desc, err) { console.log(desc); console.log(err); } }); } }; function uploadFileInBlocks() { if (totalBytesRemaining > 0) { console.log("current file pointer = " + currentFilePointer + " bytes read = " + maxBlockSize); var fileContent = selectedFile.slice(currentFilePointer, currentFilePointer + maxBlockSize); var blockId = blockIdPrefix + pad(blockIds.length, 6); console.log("block id = " + blockId); blockIds.push(btoa(blockId)); reader.readAsArrayBuffer(fileContent); currentFilePointer += maxBlockSize; totalBytesRemaining -= maxBlockSize; if (totalBytesRemaining < maxBlockSize) { maxBlockSize = totalBytesRemaining; } } else { commitBlockList(); } } function commitBlockList() { var uri = submitUri + '&comp=blocklist'; console.log(uri); var requestBody = '<?xml version="1.0" encoding="utf-8"?><BlockList>'; for (var i = 0; i < blockIds.length; i++) { requestBody += '<Latest>' + blockIds[i] + '</Latest>'; } requestBody += '</BlockList>'; console.log(requestBody); $.ajax({ url: uri, type: "PUT", data: requestBody, beforeSend: function (xhr) { xhr.setRequestHeader('x-ms-blob-content-type', selectedFile.type); xhr.setRequestHeader('Content-Length', requestBody.length); }, success: function (data, status) { console.log(data); console.log(status); }, error: function (xhr, desc, err) { console.log(desc); console.log(err); } }); } function pad(number, length) { var str = '' + number; while (str.length < length) { str = '0' + str; } return str; } </script> </head> <body> <form> <div style="margin-left: 20px;"> <h1>File Uploader</h1> <p> <strong>SAS URI</strong>: <br/> <span class="input-control text"> <input type="text" id="sasUrl" style="width: 50%" value=""/> </span> </p> <p> <strong>File To Upload</strong>: <br/> <span class="input-control text"> <input type="file" id="file" name="file" style="width: 50%"/> </span> </p> <div id="output"> <strong>File Properties:</strong> <br/> <p> Name: <span id="fileName"></span> </p> <p> File Size: <span id="fileSize"></span> bytes. </p> <p> File Type: <span id="fileType"></span> </p> <p> <input type="button" value="Upload File" onclick="uploadFileInBlocks()"/> </p> <p> <strong>Progress</strong>: <span id="fileUploadProgress">0.00 %</span> </p> </div> </div> <div> </div> </form> </body> </html>
Некоторые предостережения
This makes use of HTML5 File API and while all new browsers support that, same can’t be said about older browsers. If your users would be accessing an application like this using older browsers, you would need to think about alternative approaches. You could either make use of SWF File Uploader or could write an application using Silverlight. Steve Marx wrote a blog post about uploading files using Shared Access Signature and Silverlight which you can read here: http://blog.smarx.com/posts/uploading-windows-azure-blobs-from-silverlight-part-1-shared-access-signatures.
I found the code working in IE 10, Google Chrome (version 24.0.1312.57 m) on my Windows 8 machine. I got error when I tried to run the code in FireFox (version 18.0.2) and Safari (version 5.1.7) browsers so obviously one would need to keep the browser incompatibility in mind.
Enhancements
I hacked this code in about 4 hours or so and obviously my knowledge is somewhat limited when it comes to JavaScript and CSS so a lot can be improved on that front . However some other features I could think of are:
Generate SAS on demand: You could possibly have a server side component which would generate SAS URI on demand instead of having a user enter that manually.
Multiple file uploads: This application can certainly be extended to upload multiple files. A user would select multiple files (or may be even a folder) and have the application upload multiple files.
Drag/drop support: This application can certainly be extended to support drag/drop scenario where users could drag files from their desktop and drop them to upload.
Do upload in Web Worker: This is another improvement that can be done where uploads are done through web worker capability in HTML5.
Parallel uploads: Currently the code uploads one chunk at a time. A modification could be to upload multiple chunks simultaneously.
Transient error handling: Since Windows Azure Storage is a remote shared resource, you may encounter transient errors. You could modify the application to handle these transient errors. For more details on transient errors, please see this blog post of mine: http://gauravmantri.com/2013/01/11/some-best-practices-for-building-windows-azure-cloud-applications/.
Summary
So that’s it for this post! As you saw, it is quite easy to implement a very simple HTML/JS based application for getting data into Windows Azure Blob Storage. Obviously there’re some limitations and there’s cross-browser compatibility issues one would need to consider but once those are sorted out, it opens up a lot of exciting opportunities. I hope you’ve found this post useful. As always, if you find any issues with the post please let me know and I’ll fix it ASAP.
Happy Coding!