Сайт Игоря Кононученко   Статьи

Перетаскивание файлов в браузер (Drag and Drop, XMLHttpRequest)

22 апреля 2010
Поддерживает браузеры:
Хроме Фаерфоксе
Бросайте файлы сюда
Файлы на сервере не сохраняю, поэтому адреса для скачивания работать не будут.

Как долго я этого ждал!

С самого первого знакомства с инетом я не мог понять почему нельзя делать загрузку путем перетаскивания файла в браузер. Узнав о том, что Гугл у себя в почте реализовал подобный функционал, я практически сразу захотел сделать такое же в своих проектах.

Файловый обменник

Мы в студии постоянно обмениваемся файлами. До текущего момента обмен происходил либо через мыло, либо через скайп. Это создает ряд неудобств. Невозможно ни с кем другим удобно поделиться файлом и приходится совершать кучу лишних телодвижений.

Именно на проекте, который решает задачу обмена файлами я и решил опробовать новые возможности.

Браузеры

Пока только последние версии Фаерфокса и Хрома поддерживают данный функционал, да и то с оговорками, о которых пойдет речь ниже.

Реализация

Разработка состояла из нескольких этапов: обработки брошенных в браузер файлов, отправки и обработки на стороне сервера, отображения файлов в списке, удаления файлов и рендеринга списка уже загруженных файлов. Посмотрим подробнее на интересные моменты.

Обработка брошенных в браузер файлов. Подвязываемся к нужным событиям (все обязательны):

1
2
3
4
5
6
7
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;
}
Честно подсмотрено у Степана Резникова в его примере по его же наводке. После этого пришлось рефакторить код и статью. Степану спасибо.

Код, который не дает бросить файл мимо корзины:

1
2
3
4
5
6
7
8
9
$(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) приходит содержание файла в чистом виде. Поэтому приходится делать дополнительные телодвижения:

1
2
3
4
5
6
filename = request.GET["file"]
//содержание файла в чистом виде
data = request.raw_post_data

//мой класс сохраняет файл и отдает сохраненное имя
name = fm.save_file(filename, data)

Код сохранения:

1
2
3
4
5
6
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)

В догонку

Круто-круто, что наконец-то можно удобно бросать файлы в браузер. Эта штука реально меняет веб. Собираюсь во всех проектах, где нужна закачка файлов, реализовать такой функционал.

Далее. Сколько раньше не смотрел статей на тему аяксовой закачки файлов, всегда приходилось серверную часть самому додумывать. Чтобы максимально упросить жизнь своим читателям, выкладываю весь код с потрохами в виде зип-архива. Пользуйтесь и радуйте своих пользователей.

Все вопросы и пожелания рад буду получить на почту или в Твиттер.

Про создание этого сайта
Движок презентации в браузере
SmartInterval — класс для эффективной обработки повторяющихся событий
Ctrl
Анимированный баннер средствами CSS3