Читаем файлы, расположенные локально при помощи JavaScript


Возможности, появляющиеся в браузерах, всё более удивляют и восхищают веб-разработчиков. То, что раннее реализовывалось при помощи flash’a или апплетов, сейчас можно написать на чистом js. Одной из таких удивительных возможностей стало возможность чтения файлов, расположенных локально.

Зачем это нужно

Вариантов использования этой, безусловно, полезной возможности превеликое множество:

  • Сжатие (архивация) файлов перед отправкой на сервер.
  • Сжатие (с потерей качества), изменение размера или обрезка при помощи canvas изображения перед отправкой на сервер.
  • Предоставление возможности пользователю самому определить шрифт текста, если файл шрифта расположен у него на компьютере.
  • Можно перед загрузкой на сервер определить MIME тип файла и отфильтровать файлы, который нельзя загружать на сервер. (Например, у вас фотохостинг и вы не хотите, чтобы туда грузили что-то, кроме изображений) Это можно сделать и на сервере, но это отнимет время у пользователя и увеличит нагрузку на сервер.
  • Можно написать приложение, работающее в браузере. Например, графический редактор, или редактор js (html) – кода, в которых будет предусмотрена возможность сохранения данных на стороне клиента.

Проверка поддержки

Для пользователей, не желающих идти в ногу со временем и использующих устаревшие браузеры или IE, необходимо предусмотреть альтернативную реализацию того функционала, который написан с использованием file API.

Для проверки того, поддерживает ли браузер file API полностью необходимо просто проверить наличие таких переменных как File, FileReader, FileList и Blob в глобальной области видимости (в свойствах объекта window)

1
2
3
4
var fileAPISupport = false;
if(window.File && window.FileReader && window.FileList && window.Blob) {
  fileAPISupport = true;
}

После выполнения вышеуказанного кода, в переменной fileAPISupport будет записано булево значение, указывающее на то, поддерживается file API в браузере, в котором был выполнен этот код, или нет.

Безопасность

Естественно, что читать файлы без ведома пользователя вам вряд ли удастся. Для того, чтобы прочитать файл(ы), необходимо, чтобы пользователь сам его (их) выбрал и нажал кнопку «Открыть»

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

Выбор файлов для чтения

Чтобы предоставить пользователю возможность выбирать файлы для чтения с помощью js, необходимо использовать html-елемент

<input type=”file” />

Для выбора нескольких файлов к этому элементу необходимо добавить атрибут multiple со значением true.

Для того, чтобы получить объект для работы с файлом в js необходимо обратится к свойству files input’а при помощи которого пользователь выбирал файлы. Это свойтcво содержит список файлов (FileList), который выбрал пользователь. FileList очень похож на массив, у него есть длина и к записанным в него файлам можно обращаться по индексу, как к элементам массива. Объект для работы с файлом (File) содержит в себе размер файла в байтах (свойство size), его MIME тип (type), имя (name) и дату последней модификации (lastModifiedDate).

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

html-код:

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
<html>
<head>
<title>Выберите файлы</title>
<style>
table, td {
  border: 1px solid lightgray;
}
 
table {
  padding: 2px;
  margin-top: 10px;
}
 
td {
  padding: 5px;
}
</style>
<script type="text/javascript" src="selectingFiles.js"></script>
</head>
 
<body>
<input type="file" multiple="true" /><br />
<div id="output">
Файлов не выбрано
</div>
</body>
 
</html>

и js:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/*
 * Функция для добавления строки (<tr>),   
 * содержащей информацию о файле
 * в тело таблицы (tbody)
 */
 
function appendFileInfo(tbody, data) {
  var tr = document.createElement('tr');
  for(var j = 0; j < data.length; j++) {
      td = document.createElement('td');
      td.innerHTML = data[j] || 'неизвестно';
      tr.appendChild(td);
    }
    tbody.appendChild(tr);
  return tbody;
}
 
/*
 * Функция для создания превью, т.е
 * определение его размеров 
 * по исходным размером изображения
 */
 
function makePreview(image, a) {
  var img = image,
    w = img.width, h = img.height,
    s = w / h;     
 
  if(w > a && h > a) {
    if(img.width > img.height) {
      img.width = a;
      img.height = a / s;
    } else {
      img.height = a;
      img.width = a * s;
    }
  }
 
  return img;
}
 
/*
 * Эту функцию мы будем вызывать при изменении (onchange)
 * input'а, т.е. когда пользователь выберет файлы.
 */
 
function onFilesSelect(e) {
  // получаем объект FileList
  var files = e.target.files,
    // div, куда помещается таблица с информацией о файлах
    output = document.getElementById('output'),
    // таблица с информацией
    table = document.createElement('table'),
    // её тело
    tbody = document.createElement('tbody'),
    // строка с информацией о файле (Перезаписывается каждый шаг цикла)
    row,
    // FileReader (Создаётся для каждого файла)
    fr,
    // объект file из FileList'a
    file,
    // массив с информацией о файле
    data;
 
  // Чистим контейнер с таблицей
  output.innerHTML = '';
 
  // Вставляем в таблицу её тело
  table.appendChild(tbody);
  // Определяем заголовок таблицы (Названия колонок)
  tbody.innerHTML = 
  "<tr><td>Имя</td><td>MIME тип</td><td>Размер (байт)</td><td>Превью</td></tr>";
 
  // Перебираем все файлы в FileList'е
  for(var i = 0; i < files.length; i++) {    
    file = files[i];
    // Если в файле содержится изображение
    if(/image.*/.test(file.type)) {
      // узнаём информацию о нём
      data = [file.name, file.type, file.size];
      fr = new FileReader();
      // считываем его в строку base64
      fr.readAsDataURL(file);
      // как только файл загружен
      fr.onload = (function (file, data) {
        return function (e) {         
          var img = new Image(),             
            s, td;       
          img.src = e.target.result;
 
          /*
           * и как только загружено изображение
           * добавляем в информацию о файле html-код первьюшки
           */
 
          if(img.complete) {
            img = makePreview(img, 128);
            data.push('<img src="' + img.src + '" width=' + img.width + '" height="' + img.height + '" />');
            appendFileInfo(tbody, data);
          } else {
            img.onload =  function () {
              img = makePreview(img, 128);
              data.push('<img src="' + img.src + '" width=' + img.width + '" height="' + img.height + '" />');
              appendFileInfo(tbody, data);
            }
          }
 
        }
      }) (file, data);
    // Если файл не изображение
    } else {
      // то вместо превью выводим соответствующую надпись
      data = [file.name, file.type, file.size, 'Файл не является изображением'];
      appendFileInfo(tbody, data);
    }      
  }
  // помещаем таблицу с информацией о файле в div
  output.appendChild(table);  
}
 
// проверяем поддерживает ли браузер file API
if(window.File && window.FileReader && window.FileList && window.Blob) {
  // если да, то как только страница загрузится
  onload = function () {
    // вешаем обработчик события, срабатывающий при изменении input'а
    document.querySelector('input').addEventListener('change', onFilesSelect, false);
  }
// если нет, то предупреждаем, что демо работать не будет
} else {
  alert('К сожалению ваш браузер не поддерживает file API');
}


Демо

Обратите внимание на то, что для того, чтобы присвоить FileReader’у обработчик onload, необходимо было создать замыкание для удержания ссылок на текущие переменные data и file. Если его бы не было, то при срабатывании обработчика в переменных file и data содержались данные о последнем файле.

Мы считывали файлы в base64. Это было сделано для того, чтобы можно было отобразить превьюшку на картинку, если файл являлся изображение. Но читать файлы можно и как двоичную строку (каждый символ в такой строке имеет код от 0 до 255) при помощи функции readAsBinaryString объекта FileReader, и как текст при помощи readAsText (в кодировке UTF-8 по умолчанию), и как ArrayBuffer (объект похожий на массив, который может содержать только значения от 0 до 255) при помощи readAsArrayBuffer.

Drag and drop

Для того, чтобы выбрать файл для последующего чтения можно просто перетащить его из окна файлового менеджера в указанную область на html-странице, открытой в браузере. К сожалению, эта возможность не поддерживается оперой даже последней на момент выхода статьи версией (11.61)

Следующий пример демонстрирует эту возможность.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<html>
<head>
<style>
#dropZone {
  width: 600px;
  height: 200px;
  margin: 10px;
  border: dotted black 2px;
}
</style>
<title>Drag And Drop</title>
<script type="text/javascript">
  // Проверяем поддерживает ли браузер drag and drop
  if('ondrop' in document.createElement('div')) {
    onload = function () {
      var dropZone = document.getElementById('dropZone');
 
      /*
       * Обработчик, срабатывающий, когда курсор с
       * перетаскиваем объектом оказывается над dropZone
       */
 
      dropZone.addEventListener('dragover', function (e) {
        // Останавливаем всплытие события
        e.stopPropagation();
        // останавливаем действие по умолчанию, связанное с эти событием.
        e.preventDefault();
        e.dataTransfer.dropEffect = 'copy';    
      }, false);
 
      /*
       * Обработчик, срабатывающий, когда мы
       * бросаем перетаскиваемые файлы в dropZone
       */
 
      dropZone.addEventListener('drop', function (e) {
 
        e.stopPropagation();
        e.preventDefault();
 
        var files = e.dataTransfer.files, info = '', file;
 
        for(var i = 0; file = files[i]; i++) {
          info += [file.name, '(', file.type, ')', '-', file.size, 'байт'].join(' ') + '\n';
        }
 
        alert(info);
 
      }, false);
    }
  // очень печально если браузер не поддерживает drag and drop
  } else {
    alert("К великой печали ваш браузер не поддерживает Drag&Drop(");
  }
</script>
</head>
 
<body>
<div id="dropZone">
</div>
</body>
 
</html>


Демо

Читаем файлы по частям

Не обязательно считывать весь файл сразу, можно прочитать несколько байт из любой части файла. Оперой эта возможность поддерживается только с версии 11.61.

Номер байта, с которого будет начинаться чтение файла, и номер байта, каким чтение будет заканчиваться, определяется при помощи функций объекта Blob webkitSlices(startByte, endByte) для хрома и mozSlice(startByte, endByte) для лисы. Для того чтобы считать часть файла возвращаемый этими функциями объект Blob передаётся, например, функции readAsBinaryString объекта FileReader.

Следующий пример демонстрирует возможность читать часть файла.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<html>
<head>
<title>Выберите файл</title>
<script type="text/javascript">
  // Номер байта с которого будем начинать чтение
  var start = 0;
 
  // Функция для чтения части файла
  function read(startByte, stopByte) {
    var fr = new FileReader(),
      file = document.querySelector('input').files[0],
      blob;
 
    if(!file) {
      alert('Выберите, пожалуйста, файл!');
      return;
    }
 
    fr.onloadend = function (e) {
 
      /*
       * Необходимо проверять статус готовности перед тем, 
       * как получать считанный результат 
       */
 
      if(e.target.readyState == FileReader.DONE) {
        output.textContent += e.target.result;
      }
    }
 
    // Для хрома
    if (file.webkitSlice) {
      blob = file.webkitSlice(startByte, stopByte + 1);
    // Для лисы
    } else if (file.mozSlice) {
      blob = file.mozSlice(startByte, stopByte + 1);
    }
 
    // Считываем как бинарную строку
    fr.readAsBinaryString(blob);
 
    // Меняем начальную позицию, учитываем что байты нумеруются с нуля
    start = stopByte + 1;
  }
 
  // Проверяем поддерживается ли возможность читать файл по кускам
  if(!!window.Blob && (!!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice)) {
    onload = function () {
      output = document.getElementById('output');
    }
  // и предупреждаем пользователя если не поддерживается
  } else {
    alert('Ваш браузер не поддерживает чтение файлов по частям!');
  }
</script>
</head>
 
<body>
<input type="file" /><br />
<button id="fiveByte" onclick="read(start, start + 4)">Считать 5 байт</button>
<button id="tenByte" onclick="read(start, start + 9)">Считать 10 байт</button>
<button id="twentyByte" onclick="read(start, start + 19)">Считать 20 байт</button>
<div id="output">
</div>
</body>
 
</html>


Демо

Мониторим прогресс чтения

File API предоставляет возможность отследить прогресс чтения локального файла. У FileReader’а есть обработчики событий onloadstart и onprogress, срабатывающие при начале загрузки и прогрессе чтения файла. Кроме того, при помощи обработчика onerror можно обрабатывать ошибки, возникшие в ходе чтения файла.

Покажем всё это на следующем примере.

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
42
43
44
45
46
47
48
49
50
51
52
<html>
<head>
<title>Выберите файл</title>
<script type="text/javascript">
if(window.File && window.FileReader && window.FileList && window.Blob) {
  onload = function () {
    var inp = document.querySelector('input'),
      progressBar = document.querySelector('progress');
 
    inp.addEventListener('change', function (e) {
      var file = e.target.files[0],
        fr = new FileReader();
 
        fr.onprogress = function (e) {          
          var loaded = Math.round((e.loaded / e.total) * 100);
          progressBar.value = loaded;          
        }
 
        fr.onload = function () {
          alert("файл считан!");
        }
 
        fr.onerror = function (e) {
          switch(e.target.error.code) {
            case e.target.error.NOT_FOUND_ERR:
              alert('Файл не найден!');
              break;
            case e.target.error.NOT_READABLE_ERR:
              alert('Невозможно прочитать файл!');
              break;
            case e.target.error.ABORT_ERR:
              break;
            default:
              alert('Ошибка при чтении файла!');
          }
        }
 
        fr.readAsBinaryString(file);
    }, false);
  }
} else {
  alert("Ваш браузер не поддерживает file API");
}
</script>
</head>
 
<body>
<input type="file" /><br />
<progress style="width: 300px; margin: 10px; " value="0" min="0" max="100"></progress>
</body>
 
</html>

Демо

Сегодня вы познакомились ещё с одной удивительной возможностью, которую предоставляют нам новые технологии и, хотя, всё это пока не находит широкого применения, знать об этом не помешает. Прогресс не стоит на месте и в скором времени у всех пользователей будут нормальные браузеры поддерживающие не только file API, но и ещё много других удивительных возможностей.

Скачать исходники

  • Спасиб за ценную статью. не мог разобраться как читать бинарный текст из вебсокета, представленный в виде blob на javascript

  • константин

    а как узнать полный путь до файла ?

  • Спасибо! Отличный пример. Помогите пожалуйста вставить ссылку на сам файл, чтобы его можно было открыть, кликнув по ссылке.
    Например, отображается список файлов *.xls со ссылками. Кликнув — открывается файл в приложении Microsoft Excell. Заранее очень благодарен!

    • К сожалению, из-за политики безопасности, узнать путь к локальному файлу при помощи js нельзя.