Поддерживает браузеры:
Файлы на сервере не сохраняю, поэтому адреса для скачивания работать не будут.
Как долго я этого ждал!
С самого первого знакомства с инетом я не мог понять почему нельзя делать загрузку путем перетаскивания файла в браузер. Узнав о том, что Гугл у себя в почте реализовал подобный функционал, я практически сразу захотел сделать такое же в своих проектах.
Файловый обменник
Мы в студии постоянно обмениваемся файлами. До текущего момента обмен происходил либо через мыло, либо через скайп. Это создает ряд неудобств. Невозможно ни с кем другим удобно поделиться файлом и приходится совершать кучу лишних телодвижений.
Именно на проекте, который решает задачу обмена файлами я и решил опробовать новые возможности.
Браузеры
Пока только последние версии Фаерфокса и Хрома поддерживают данный функционал, да и то с оговорками, о которых пойдет речь ниже.
Реализация
Разработка состояла из нескольких этапов: обработки брошенных в браузер файлов, отправки и обработки на стороне сервера, отображения файлов в списке, удаления файлов и рендеринга списка уже загруженных файлов. Посмотрим подробнее на интересные моменты.
Обработка брошенных в браузер файлов. Подвязываемся к нужным событиям (все обязательны):
| this.el.bind("dragover", this._over.bind(this))//подсвечиваю область для бросания
.bind("dragenter", function(){return false;})//просто обрабатываю вхолостую событие
.bind('dragleave', this._leave.bind(this))//тушим подсветку
.bind("drop", this._drop.bind(this))//обработчик бросания на области
//не даем пользователю бросить файл мимо области бросания
this.blockDocumentDrop();
|
Ниже обработчики. В каждом из них мы должны стопнуть событие вернув false:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | //тушим подсветку
_leave:function(e)
{
return this.hideHighlight();
},
//подсвечиваем
_over: function(e)
{
var dt = e.originalEvent.dataTransfer;
if(!dt) return;
//проверяем, что бросают именно файлы
//FF
if(dt.types.contains&&!dt.types.contains("Files")) return;
//Chrome
if(dt.types.indexOf&&dt.types.indexOf("Files")==-1) return;
//без этого в Хроме не работает
if($.browser.webkit) dt.dropEffect = 'copy';
this.el.addClass(this.mousein_class);
return false;
},
//обрабатываем бросание
_drop:function(e)
{
var dt = e.originalEvent.dataTransfer;
if(!dt&&!dt.files) return;
this.hideHighlight();
var files = dt.files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
//а в Фаерфоксе еще есть file.name, а в Хроме - нету
this.onDropFile(e, file.fileName);
this.upload(file);
}
return false;
}
|
Честно подсмотрено у Степана Резникова в
его примере
по его же наводке. После этого пришлось рефакторить код и статью. Степану спасибо.
Код, который не дает бросить файл мимо корзины:
| $(document)
.bind('dragenter', function(e) {return false;})
.bind('dragleave', function(e) {return false;})
.bind('dragover', function(e) {
var dt = e.originalEvent.dataTransfer;
if (!dt) { return; }
dt.dropEffect = 'none';
return false;
}.bind(this));
|
Отправка и обработка на стороне сервера. Единственно работающим и отправляющим файл и в Хроме и Фаерфоксе оказался такой код:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | upload: function(file)
{
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", function(e){this.onProgress(e, file.fileName)}.bind(this), false);
//попытки заставить работать событие прогресса закачки в Хроме. Так и не вышло
//xhr.upload.onprogress = function(e){debugger;this.onProgress(e, file.fileName)}.bind(this);
//xhr.onprogress = function(e){debugger;this.onProgress(e, file.fileName)}.bind(this);
xhr.onload = function(e){this.onComplete(e, file.fileName)}.bind(this);
//параметром адреса передаю имя файла - иначе никак
xhr.open('POST', this.url+"?file="+file.fileName, true);
xhr.send(file);
}
|
На сервер (я использую Django) приходит содержание файла в чистом виде. Поэтому приходится делать дополнительные телодвижения:
| filename = request.GET["file"]
//содержание файла в чистом виде
data = request.raw_post_data
//мой класс сохраняет файл и отдает сохраненное имя
name = fm.save_file(filename, data)
|
Код сохранения:
| def save_file(self, filename, data):
filename = self.rename_file(filename)
file = open(os.path.join(self.dir, filename), 'w')
file.write(data)
file.close()
return filename
|
Итоговая структура
После того как я отрефакторил начальный код, вырисовалась понятная логическая структура.
Клиентский код
DragUpload - класс, который занимается подсветкой области бросания и обработкой файлов (прием и отправка).
FileLine - инкапсуляция логики строки из списка файлов (рендеринг, обработка событий).
FileManager - главный класс, который все синхронизирует и всем управляет.
Код, который постоянно и везде нужен, но никогда не можешь вспомнить где его уже писал :-)
Серверный код. Работу с файлами на сервере выполняет класс FileManager. Пожалуй, интересен рекурсивный метод переименования файла:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | def get_indexed_name(self, filename):
if not os.path.exists(os.path.join(self.dir, filename)):
return filename
index = 1
name, ext = os.path.splitext(filename)
filename_regexp = "^(.*?)(_\d+)%s$" % ext.replace(".", "\.")
match = re.match(filename_regexp, filename)
if match:
_index = int(match.group(2).replace('_', ''))
name = match.group(1)
index = _index+1
new_name = name + '_' + unicode(index)+ ext
return self.get_indexed_name(new_name)
|
В догонку
Круто-круто, что наконец-то можно удобно бросать файлы в браузер. Эта штука реально меняет веб. Собираюсь во всех проектах, где нужна закачка файлов, реализовать такой функционал.
Далее. Сколько раньше не смотрел статей на тему аяксовой закачки файлов, всегда приходилось серверную часть самому додумывать. Чтобы максимально упросить жизнь своим читателям, выкладываю весь код с потрохами в виде зип-архива. Пользуйтесь и радуйте своих пользователей.
Все вопросы и пожелания рад буду получить на почту или в Твиттер.