Безпечне завантаження файлів. Створюємо прототип

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Декілька років тому я на співбесіді отримав тестове завдання — реалізувати безпечне завантаження файлів на сервер. Воно мені так сподобалось, що у вільний від роботи час я почав експериментувати над суттєвим розширенням його можливостей. Пізніше я не раз про це шкодував, адже кожна моя нова ідея призводила до зміни архітектури та значної частини коду.


Безпечне завантаження файлів

Коли я прочитав завдання, то спершу припустив, що мова йде про звичайну HTML-форму для завантаження файлів на сервер. Пригадую, як дуже зрадів цьому, адже з такою легкою перевіркою на співбесіді, я однозначно отримаю роботу. Однак майже відразу мій внутрішній голос почав наполягати, що не може бути все так просто й інтуїція мене не підвела.

Дослідивши це питання в інтернеті, я знайшов низькорівневий метод Blob.slice() з File API. З його допомогою можна отримати частину обраного користувачем файлу, вказавши необхідні значення зміщення початку та кінця фрагменту відносно початку файлу. Це дає можливість, наприклад, організувати контрольовану передачу файлу порціями, поєднуючи їх на сервері.

Вимоги

Спершу необхідно реалізувати функцію повторного відправлення фрагменту файлу в разі виникнення проблем та функцію дозавантаження. Також обовʼязково додати можливість користувачу керувати процесом завантаження за допомогою стандартних кнопок «Завантажити», «Призупинити», «Продовжити», «Скасувати». Та й чому б не налагодити отримання вичерпної динамічної інформації про перебіг процесу завантаження для зручного інформування користувача.


Інформаційне вікно
Інформаційне вікно процесу завантаження файлів

Окреме важливе питання — це методологія визначення оптимального розміру частин файлу, на які його необхідно ділити під час завантаження на сервер. Файли малого розміру краще ділити на маленькі порції, а файли великого розміру — відповідно на великі. Також необхідно зважати на пропускну здатність каналу, яка може мінятися динамічно через інше одночасне паралельне завантаження.

У разі виникнення помилок, під час відправлення порцій файлу, їхній розмір бажано зменшити, щоби зменшити ймовірність виникнення повторної помилки. І взагалі поняття маленький та великий — дуже відносні та абстрактні, скільки конкретно це в байтах? Так у мене виникла ідея створити алгоритм динамічного адаптивного визначення оптимального розміру фрагменту файлу.


Консоль інтернет-оглядача
Виведення детальної інформації про фрагменти в консолі

Ще варто отримувати деталізовану інформацію про завантаження кожної порції файлу окремо для виводу її в консоль з метою зневадження. Окрім цього, в мене ще була ідея реалізувати підвищений контроль за фрагментами файлу за допомогою технології блокчейн. Але через використання захищеного протоколу HTTPS та за браком бажання зберігати додаткові проміжні дані завантаження на сервері, вирішив від неї відмовитись.

Реалізація

Для реалізації завантажувача необхідно створити скрипти, як на клієнті — зчитування та передача фрагментів файлу, так і на сервері — отримання та збереження фрагментів у файл. Клієнт ділить обраний користувачем файл на частини, відправляє їх, керує процесом на вимогу користувача та відображає інформацію про завантаження. Сервер виконує команди клієнта, отримує фрагменти файлу та поєднує їх, записуючи в тимчасовий файл, а після закінчення переміщує його в зазначену теку.


Схема файлів завантажувача
Схема файлів завантажувача

Для повноцінної роботи завантажувача нам необхідно 5 файлів, кожен із яких виконує свою певну роль. Я завантажив їх усіх на GitHub в теку SafeUpload.

index.htmПриклад інтеграції завантажувача в HTML-сторінку
index.jsПриклад інтеграції завантажувача в JS-скрипти
Upload.jsПриклад реалізації прототипа завантажувача
api.phpПриклад реалізації інтерфейсу для сервера
File.phpПриклад реалізації операцій над файлом на сервері

Файл index.htm містить розмітку елемента вибору файлу та вікна індикації процесу завантаження, підключення зовнішніх скриптів завантажувача та стилів Bootstrap. У скрипті index.js розміщені налаштування завантажувача, функції зворотного виклику, реакції на події завантажувача та допоміжні функції. Код файлу Upload.js реалізовує функції поділу файлу на частини та відправлення їх на сервер, керування процесом завантаження, агрегації статистичної інформації та інше.

Файл api.php, якщо не звертати увагу на допоміжні функції, виконує роль маршрутизатора та контролера для обʼєкта File з однойменного файлу. А вже він зі свого боку містить увесь необхідний інструментарій для створення файлу, наповнення його фрагментами, переміщення та видалення в разі потреби. Далі я описуватиму лише основний файл Upload.js, тому що опис решти файлів у статтю не поміститься і їх необхідно буде досліджувати самостійно.

Властивості та конструктор

Створімо клас Upload та додамо в нього для початку тільки властивості з детальним описом та конструктор.


class Upload {
    /**
     * @property {object}   #settings                    - Налаштування за замовчуванням
     * @property {string}   #settings.api                - Адреса API для завантаження файлу
     * @property {number}   #settings.chunkSizeMinimum   - Мінімальний розмір частини файлу, байти
     * @property {number}   #settings.chunkSizeMaximum   - Максимальний розмір частини файлу, байти
     * @property {number}   #settings.fileSizeLimit      - Максимальний розмір файлу, байти
     * @property {number}   #settings.interval           - Рекомендована тривалість запиту, секунди
     * @property {number}   #settings.timeout            - Максимальна тривалість запиту, секунди
     * @property {number}   #settings.retryLimit         - Максимальна кількість повторних запитів
     * @property {number}   #settings.retryDelay         - Тривалість паузи між повторними запитами, секунди
     */
    #settings = {
        api: 'api', chunkSizeMinimum: 1024, chunkSizeMaximum: 1024 ** 2, fileSizeLimit: 1024 ** 2,
        interval: 1, timeout: 5, retryLimit: 5, retryDelay: 1
    }

    /**
     * @property {object} #fileList                - Перелік FileList з файлами File та додатковими параметрами
     * @property {object} #fileList.files          - Перелік FileList
     * @property {object} #fileList.size           - Дані про розмір всіх файлів
     * @property {number} #fileList.size.uploaded  - Розмір завантажених частин файлів, байти
     * @property {number} #fileList.size.total     - Загальний розмір всіх файлів, байти
     * @property {number} #fileList.current        - Номер поточного файлу
     */
    #fileList = {files: {}, size: {uploaded: 0, total: 0}, current: 0}

    /**
     * @property {object} #file                 - Файл File з даними та додатковими параметрами
     * @property {string} #file.name            - Назва файлу
     * @property {string} #file.type            - Тип файлу
     * @property {number} #file.size            - Розмір файлу, байти
     * @property {string} #file.uuid            - UUID файлу
     * @property {number} #file.lastModified    - Дата останньої зміни файлу, мілісекунди
     */
    #file = {name: '', type: '', size: 0, uuid: '', lastModified: 0}

    /**
     * @property {object}   #chunk                  - Параметри частини файлу
     * @property {number}   #chunk.number           - Порядковий номер частини файлу
     * @property {number}   #chunk.offset           - Зміщення від початку файлу, байти
     * @property {object}   #chunk.size             - Дані розміру частини файлу, байти
     * @property {number}   #chunk.size.base        - Розмір бази частини файлу, байти
     * @property {number}   #chunk.size.value       - Запланований розмір частини файлу, байти
     * @property {number}   #chunk.size.coefficient - Коефіцієнт розміру частини файлу (1|2)
     * @property {object}   #chunk.value            - Вміст частини файлу
     * @property {number}   #chunk.value.size       - Реальний розмір частини файлу, байти
     * @property {string}   #chunk.value.type       - Тип частини файлу
     */
    #chunk = {number: 0, offset: 0, size: {base: 0, value: 0, coefficient: 1}, value: {size: 0, type: ''}}

    /**
     * @property {object}   #request        - Запит до сервера
     * @property {string}   #request.action - Тип запиту
     * @property {object}   #request.data   - Дані запиту
     * @property {number}   #request.time   - Час виконання запиту, мс
     * @property {number}   #request.speed  - Швидкість виконання запиту, Б/с
     * @property {boolean}  #request.retry  - Ознака виконання повторних запитів
     */
    #request = {action: '', data: {}, time: 0, speed: 0, retry: true}

    /**
     * @property {object}   #events         - Ознаки деяких дій
     * @property {boolean}  #events.pause   - Ознака призупинення завантаження
     * @property {boolean}  #events.stop    - Ознака зупинки завантаження
     */
    #events = {pause: false, stop: false}

    /**
     * @property {object} #timers           - Збережені часові мітки
     * @property {number} #timers.start     - Часова мітка початку завантаження, секунди
     * @property {number} #timers.pause     - Часова мітка призупинення завантаження, секунди
     */
    #timers = {start: 0, pause: 0}

    /**
     * @property {object}   #callbacks              - Функції зворотного виклику
     * @property {function} #callbacks.pause        - Дії в разі призупинення процесу завантаження файлу
     * @property {function} #callbacks.iteration    - Дії в разі виконання кожного запита на сервер
     * @property {function} #callbacks.timeout      - Дії в разі відсутності відповіді від сервера
     * @property {function} #callbacks.resolve      - Дії в разі вдалого завершення процесу завантаження файлу
     * @property {function} #callbacks.reject       - Дії в разі невдалого завершення процесу завантаження файлу
     * @property {function} #callbacks.finally      - Дії в разі завершення процесу завантаження файлу
     */
    #callbacks = {
        pause: () => {}, iteration: () => {}, timeout: () => {},
        resolve: () => {}, reject: () => {}, finally: () => {}
    }

    /**
     * Конструктор
     * @param {object} files - Перелік FileList з файлами File
     * @param {object} [settings] - Налаштування
     * @param {object} [callbacks] - Функції зворотного виклику
     */
    constructor(files, settings = {}, callbacks = {}) {
        this.#fileList.files = files;
        for(let i = 0; i < this.#fileList.files.length; i ++)
            this.#fileList.size.total += this.#fileList.files[i].size;
        this.#settings = {...this.#settings, ...settings};
        this.#callbacks = {...this.#callbacks, ...callbacks};
        this.#file = this.#fileList.files[0];
        this.#callbacks.iteration(this.#getStatus());
    }
}

Зі всіх властивостей зміни можна вносити тільки в усталене налаштування (settings), а решта для внутрішнього використання в методах. Конструктор отримує та зберігає файли для завантаження, користувацькі налаштування, які перезапишуть усталене налаштування, та функції зворотного виклику для різних подій.

Керування завантаженням

Тепер додамо методи керування процесом завантаження: start(), pause(), resume(), та cancel().


/**
 * Починає процес завантаження файлу на сервер
 */
start() {
    this.#timers.start = this.#getTime();
    this.#open().then();
}

/**
 * Призупиняє процес завантаження файлу на сервер
 */
pause() {
    this.#request.speed = 0;
    this.#events.pause = true;
    this.#timers.pause = this.#getTime();
}

/**
 * Продовжує процес завантаження файлу на сервер
 */
resume() {
    this.#events.pause = null;
    this.#chunk.size.coefficient = 1;
    this.#chunk.size.base = this.#settings.chunkSizeMinimum;
    this.#timers.start = this.#getTime() - (this.#timers.pause - this.#timers.start);
    switch (this.#request.data.get('action')) {
        case 'open': this.#open().then(); break;
        case 'append': this.#append().then(); break;
        case 'close': this.#close().then(); break;
    }
}

/**
 * Скасовує процес завантаження файлу на сервер
 */
cancel() {
    if (this.#events.pause) {
        this.#remove();
    } else {
        this.#events.stop = true;
    }
    this.#callbacks.finally();
}

За допомогою цих публічних методів, які повʼязані з відповідними елементами на сторінці, користувачем здійснюються базові дії над завантажувачем. Загалом тут усе інтуїтивно зрозуміло й пояснювати немає сенсу, окрім використання мною оператора switch в методі resume(). Річ у тім, що я не зміг знайти спосіб динамічного формування необхідної назви приватного метода для відповідного продовження роботи завантажувача.

Керування файлом

А зараз додамо методи керування файлом open(), append(), close() та remove(), які управляються описаними вище методами. Вони виокремлені від попередніх подібних методів насамперед через те, що використовуються для рекурсії під час завантаження фрагментів файлу на сервер.


/**
 * Відкриває файл для запису на сервері
 * @see this.#request
 */
#open = async () => {
    if (this.#file.size > this.#settings.fileSizeLimit)
        return this.#error('Розмір файлу більше дозволеного');
    this.#chunk.size.base = this.#settings.chunkSizeMinimum;
    this.#request.data = new FormData();
    this.#request.data.set('action', 'open');
    this.#request.data.set('name', this.#file.name);
    this.#file.uuid = await this.#send();
    if (this.#file.uuid === undefined) return;
    this.#request.data.set('uuid', this.#file.uuid);
    await this.#append();
}

/**
 * Додає частину файлу на сервер
 * @see this.#request
 * @see this.#chunk
 */
#append = async () => {
    if (this.#events.pause) {this.#callbacks.pause();return;}
    if (this.#events.stop) {this.#remove();return;}
    this.#chunk.number ++;
    this.#chunk.size.value =
        this.#chunk.size.base * this.#chunk.size.coefficient;
    this.#chunk.value =
        this.#file.slice(this.#chunk.offset, this.#chunk.offset + this.#chunk.size.value);
    this.#request.data.set('action', 'append');
    this.#request.data.set('offset', this.#chunk.offset);
    this.#request.data.set('chunk', this.#chunk.value, this.#file.name);
    let response = await this.#send();
    if (response === undefined) return;
    this.#chunk.offset = response;
    this.#callbacks.iteration(this.#getStatus());
    let speed = Math.round(this.#chunk.value.size / this.#request.time);
    this.#fileList.size.uploaded += this.#chunk.value.size;
    if (this.#chunk.size.coefficient === 2) {
        if ((this.#request.time < this.#settings.interval) && (speed > this.#request.speed)) {
            if ((this.#chunk.size.base * 2) < this.#settings.chunkSizeMaximum)
                this.#chunk.size.base *= 2;
        } else {
            if ((this.#chunk.size.base / 2) > this.#settings.chunkSizeMinimum)
                this.#chunk.size.base /= 2;
        }
    }
    this.#request.speed = speed;
    this.#request.data.delete('chunk');
    this.#chunk.size.coefficient = 3 - this.#chunk.size.coefficient;
    if (this.#chunk.offset < this.#file.size) {
        await this.#append();
    } else {
        await this.#close();
    }
}

/**
 * Закриває файл на сервері
 * @see this.#request
 */
#close = async () => {
    this.#request.data.set('action', 'close');
    this.#request.data.set('time', this.#file.lastModified);
    let size = await this.#send();
    if (size === undefined) return;
    if (size !== this.#file.size)
        return this.#error('Неправильний розмір завантаженого файлу');
    console.debug(`Файл ${this.#file.name} завантажено`);
    this.#fileList.current ++;
    if (this.#fileList.current < this.#fileList.files.length) {
        this.#file = this.#fileList.files[this.#fileList.current];
        await this.#open();
    } else {
        this.#callbacks.resolve();
        this.#callbacks.finally();
    }
}

/**
 * Видаляє файл на сервері
 * @see this.#request
 */
#remove = () => {
    this.#request.data.set('action', 'remove');
    this.#request.retry = false;
    this.#send();

}

А ось з-поміж цих методів варто звернути особливу увагу на append(), як мінімум через використання в ньому згаданої раніше функції slice(). Але найцікавіше в ньому це реалізація автоматичного визначення оптимального розміру фрагменту файлу, запозичена мною з автомобільної сфери. Хто знає як формується суміш для інжекторних двигунів за допомогою лямбда-зонда знайде в моєму алгоритмі дещо спільне.

Алгоритм спочатку відправляє фрагмент із поточним розміром, який вказаний у налаштуваннях, або з мінімальним для першої ітерації («тік»). Потім відправляє фрагмент розміром удвічі більшим за попередній («так») та порівнює його швидкість зі швидкістю попередньої ітерації. Якщо швидкість другої ітерації більша, значення поточного розміру фрагменту в налаштуваннях збільшується удвічі й навпаки.

Після цього пара ітерацій «тік-так» повторюється, постійно підбираючи оптимальний розмір порцій, доки файл не завантажиться повністю. Мінімальний та максимальний розміри частин файлу обмежені за допомогою відповідних налаштувань та за допомогою рекомендованої тривалості виконання операції. Експериментально я прийшов до висновку, що занадто великий період виконання запиту незручний і бажано дотримуватись оптимального інтервалу (~ 1 секунда).

Пересилання даних

Ну й нарешті додамо серце завантажувача, яке формує та здійснює пересилання даних. Щоби реалізувати механізм повторного запиту (retry), відправлення даних на сервер здійснюється у два етапи.


/**
 * Формує запит до сервера та витягує дані з відповіді
 * @returns {object|void} - Відформатована відповідь сервера
 * @see this.#request
 */
#send = async () => {
    let url = this.#settings.api;
    let body = {method: 'POST', body: this.#request.data};
    let retry = (this.#request.retry) ? 1 : 0;
    this.#request.time = (new Date()).getTime();
    let response = await this.#fetchExtended(url, body, retry);
    this.#request.time = ((new Date()).getTime() - this.#request.time) / 1000;
    if (response === undefined) return;
    let responseText = await response.text();
    if (response.status === 200)
        return /^\d+$/.test(responseText) ? +responseText : responseText;
    return this.#error(responseText);
}

/**
 * Відправляє запит на сервер з timeout та retry в разі необхідності
 * @param {string} url - Адреса запиту
 * @param {object} body - Дані запиту
 * @param {number} [retry=1] - Номер повторного запиту, 0 - без повторів
 * @returns {Response|void} - Відповідь сервера при наявності
 */
#fetchExtended = async (url, body, retry = 1) => {
    try {
        let fetchPromise = fetch(url, body);
        let timeoutPromise = new Promise(resolve =>
            (setTimeout(resolve, this.#settings.timeout * 1000))
        );
        let response = await Promise.race([fetchPromise, timeoutPromise]);
        if (response) return response;
        console.warn(`Перевищено час виконання запиту (${this.#settings.timeout}c)`);
    } catch (e) {
        console.warn('Під час виконання запиту виникла помилка: ' + e.message);
    }
    if ((retry === 0) || this.#events.stop) return;
    if (retry <= this.#settings.retryLimit) {
        console.warn('Повторний запит #' + retry);
        await new Promise(resolve => {
            setTimeout(resolve, this.#settings.retryDelay * 1000)
        });
        return this.#fetchExtended(url, body, ++retry);
    } else {
        this.pause();
        this.#callbacks.pause();
        this.#callbacks.timeout();
    }
}

Метод send() здійснює підготовку запиту до сервера та опрацювання відповіді від нього, а метод fetchExtended() безпосередньо здійснює запит. Якщо запит здійснено невдало, автоматично його повторює з попередньо заданою в налаштуваннях кількістю разів та інтервалами між ними. У разі, якщо цього виявиться не достатньо завантаження не скасовується, а призупиняється, щоби його можна було продовжити пізніше, після розвʼязання проблеми.

Додаткові методи

Під кінець додамо декілька допоміжних методів.


/**
 * Вираховує та повертає дані про статус процесу завантаження файлу
 * @returns     {object}
 * @property    {number} chunk.number           - Номер частини файлу
 * @property    {number} chunk.size             - Розмір частини файлу, байти
 * @property    {number} chunk.time             - Час завантаження частини файлу, секунди
 * @property    {number} chunk.speed            - Швидкість завантаження частини файлу, байти/секунду
 * @property    {number} current.number         - Номер поточного файлу
 * @property    {string} current.name           - Назва поточного файлу
 * @property    {number} current.size.uploaded  - Розмір завантаженої частини поточного файлу, байти
 * @property    {number} current.size.total     - Загальний розмір поточного файлу, байти
 * @property    {number} total.numbers          - Загальна кількість файлів
 * @property    {number} total.size.uploaded    - Розмір завантаженої частини всіх файлів, байти
 * @property    {number} total.size.total       - Загальний розмір всіх файлів, байти
 * @property    {number} total.time.elapsed     - Час з початку завантаження файлів, секунди
 * @property    {number} total.time.estimate    - Прогнозований час до завершення завантаження файлів, секунди
 */
#getStatus = () => {
    let status = {};
    status.chunk = {
        number: this.#chunk.number,
        size: this.#chunk.value.size,
        time: this.#request.time,
        speed: this.#request.speed
    }
    status.current = {
        number:  this.#fileList.current + 1,
        name: this.#file.name,
        size: {
            uploaded: this.#chunk.offset,
            total: this.#file.size
        }
    }
    status.total = {
        number: this.#fileList.files.length,
        size: {
            uploaded: this.#fileList.size.uploaded,
            total: this.#fileList.size.total,
        },
        time: {elapsed: 0, estimate: 0}
    }
    if (this.#timers.start > 0)
        status.total.time.elapsed = this.#getTime() - this.#timers.start;
    if (status.total.size.uploaded > 0) {
        status.total.time.estimate =
            Math.round(status.total.size.total / (status.total.size.uploaded / status.total.time.elapsed))
        status.total.time.estimate -= status.total.time.elapsed;
    }
    return status;
}

/**
 * Повертає поточну мітку часу
 * @returns {number} - Мітка часу, секунди
 */
#getTime = () => {
    return Math.round((new Date()).getTime() / 1000);
}

/**
 * Виконує дії при помилці
 * @param {string} message - Текст помилки
 */
#error = (message) => {
    this.#callbacks.reject(message);
    this.#callbacks.finally();
    console.error(message);
}

Метод getStatus() повертає вичерпні дані про статус процесу завантаження файлів на цю мить, щоби вивести її в інформаційне вікно для користувача. Метод getTime() є обгорткою для однойменної функції JS, але на відміну від неї для зручності повертає час у секундах. Метод error() викликається в разі виникнення помилки, адже перехоплення помилок за допомогою try ... catch не спрацьовують для асинхронних викликів.

Висновки

У підсумку ми створили завантажувач підвищеної надійності з деталізованою індикацією, який легко впорається з завантаженням на сервер файлів великого розміру. Під час розроблення я тестував його надійність у разі вимкнення мережі в системі, втраті звʼязку (timeout) та симуляції внутрішніх проблем на сервері. Але в будь-якому разі це лише прототип, який потребує адаптації та додаткового тестування під великим навантаженням і різноманітними позаштатними ситуаціями.

Сумісність з інтернет-оглядачами перевіряв на Chrome 83 та Opera 68 для ОС Linux, на Chrome 83, Opera 58, Edge 45 та Samsung Internet 11 для ОС Android. На момент розроблення завантажувач не працював на інтернет-оглядачах Mozilla Firefox 76 (Linux) та 68 (Android) через непідтримання приватних властивостей та методів обʼєкта. Серверна частина завантажувача розроблялася та тестувалася на PHP 7.2 (Apache 2.4, ОС Linux).

Наостанок хочу звернути вашу увагу на те, що обмеження на максимальний розмір файлу на сервері не спрацює через те, що файл передається частинами, а не цілком, як зазвичай. Також адаптуйте, в разі необхідності, такі параметри налаштувань PHP як post_max_size та upload_max_filesize, які безпосередньо повʼязані з максимальним розміром фрагмента файлу. Додатково варто приділити увагу безпеці, адже в завантажувачі немає системи авторизації між клієнтом та сервером, що дає можливість для несанкціонованого втручання.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Классика же

Кандидат: Что конкретно Вы имеете в виду, говоря «скопировать»?
Интервьюер: Ну... создать новый файл, содержимое которого является копией содержимого исходного файла.
Кандидат: Нужно ли скопировать также и метаданные о времени создания и модификации оригинального файла?
Интервьюер: Нет, не нужно.
Кандидат: Должен ли файл-копия иметь то же имя, что и исходный файл?
Интервьюер: Нет.
Кандидат: Может ли файл-копия иметь то же имя, что и исходный файл?
Интервьюер: Хммм... нет.
Кандидат: Должен ли я предусмотреть защиту от атаки через подмену букв со сходными начертаниями (например, турецкой I)?
Интервьюер: Не беспокойтесь об этом.
Кандидат: Должен ли файл-копия находиться в том же каталоге, что и исходный файл? Хочу отметить, что если да — то он, вероятно, не может иметь то же самое имя. Если только речь не идёт о копировании файла самого в себя (это тоже интересный вопрос...)
Интервьюер: Да.
Кандидат: Как насчёт атрибутов файла?
Интервьюер: Скопируйте их тоже.
Кандидат: Должен ли я модифицировать атрибуты исходного файла? Если функция копирования, которую Вы просите меня написать, является частью операции резервного копирования или архивирования, то я должен сбросить архивный атрибут у исходного файла.
Интервьюер: Нет, оставьте их в покое.
Кандидат: Вы сказали, что я должен тупо скопировать атрибуты исходного файла на файл-копию. Но если архивный атрибут у исходного файла сброшен, а я его «тупо скопирую» на файл назначения, то это может ввести в заблуждение программы резервного копирования, которые могут использоваться на этом компьютере.
Интервьюер: Просто скопируйте атрибуты. Меня не волнует, как там у пользователя организовано резервное копирование.
Кандидат: Ну, мне кажется, что это не самый разумный подход при разработке программного обеспечения, которым всё-таки будут люди пользоваться, но раз Вы так настаиваете...
Интервьюер:...
Кандидат: Как насчёт атрибута «сжатый»? Ведь может оказаться, что файловая система, на которой находится каталог назначения, не поддерживает сжатие.
Интервьюер: Делайте копию несжатой.
Кандидат: Даже если исходный файл — сжатый, а в каталоге назначения поддерживается сжатие?
Интервьюер: ДА.
Кандидат: А как насчёт атрибута «зашифрованный»? Что, если исходный файл зашифрован, а в каталоге назначения не поддерживается шифрование?
Интервьюер: В этом случае не шифруйте копию.
Кандидат: Не хочу отклоняться от темы, но мне кажется, что такое поведение создаст серьёзную дырку в безопасности. Особенно в случае если файловая система, на которую мы копируем файл, поддерживает произвольные атрибуты (прямо или косвенно).
Интервьюер: Послушайте, просто скопируйте чёртов файл!
Кандидат: Как насчёт информации о создателе файла?
Интервьюер: Плевать!
Кандидат: Как насчёт информации о владельце файла?
Интервьюер: Плевать!
Кандидат: А как насчёт прав доступа? Должен ли я по-разному обрабатывать унаследованные и назначенные права?
Интервьюер: К чёрту права!
Кандидат: На какой ОС должна работать моя функция?
Интервьюер: Windows XP.
Кандидат: Home, Pro, Media Center, или какой-то их комбинации?
Интервьюер: Pro.
Кандидат: На какой пакет обновлений я могу рассчитывать?
Интервьюер: Service Pack 2.
Кандидат: То есть я могу не поддерживать предыдущие уровни обновлений?
Интервьюер: Верно.
Кандидат: Каким образом мне будет передано имя исходного файла?
Интервьюер: Как параметр.
Кандидат: Будет ли оно передано как строка, завершённая нулевым байтом, строка с длиной, или как объект?
Интервьюер: Строка, завершённая нулевым байтом.
Кандидат: Предусматривать ли ситуацию с передачей мне пустого указателя?
Интервьюер: Нет.
Кандидат: Предусматривать ли ситуацию с передачей мне пустой строки?
Интервьюер: Нет.
Кандидат: Предусматривать ли ситуацию с передачей мне неправильно сформированной строки (например, без завершающего нулевого байта)?
Интервьюер: Нет.
Кандидат: В какой кодировке будет переданное мне имя?
Интервьюер: Unicode.
Кандидат: Простите, но Unicode — это вообще-то не кодировка. Unicode-данные должны быть в какой-то конкретной кодировке — например, UTF-8, UCS-2, UTF-16...
Интервьюер: Ладно, пусть будет UTF-8.
Кандидат: Хорошо, но смею заметить, что тогда для передачи в вызов Windows API мне придётся перекодировать в UTF-16, а это несколько геморройно...
Интервьюер: Тогда UTF-16!
Кандидат: С каким порядком байтов?
Интервьюер: Ррррр... с каким хотите!
Кандидат: Предусматривать ли обработку относительных путей, или только абсолютных?
Интервьюер: Только абсолютных.
Кандидат: Есть ли какие-то особенности у передаваемых мне путей, по которыми я должен их фильтровать?
Интервьюер: Нет. Считайте, что вызывающая программа это уже сделала.
Кандидат: Как будет формироваться (или передаваться) имя файла назначения?

[... текли минуты, превращаясь в часы...]

Кандидат: Должен ли я поддерживать (или допускать) асинхронное копирование?
Интервьюер: Нет.
Кандидат: Как я должен сообщать об аварийных ситуациях — исключением или кодом возврата?
Интервьюер: Без разницы.
Кандидат: Должен ли я обрабатывать исключения, приходящие из вызываемых мною функций, или просто пропускать их к вызвавшему меня коду?
Интервьюер: Пропускайте.
Кандидат: Что, если файл назначения уже существует?
Интервьюер: Мамой клянусь, что нет.
Кандидат: То есть вызывающая программа это гарантирует?
Интервьюер: Вот именно.
Кандидат: То есть если всё-таки окажется, что он уже существует, то это значит, что гарантии нарушены, и мне следовало бы обрушить программу — что-то явно пошло не так, и мало ли что ещё она там сейчас наделает?
Интервьюер: Как хотите.
Кандидат: А как насчёт вторичных потоков данных у файла?
Интервьюер: Да делаёте что хотите, чёрт бы Вас побрал!
Кандидат: Послушайте, Вам может показаться, что я на Вас давлю, и мне очень жаль — но мне крайне важно уяснить все детали Вашего задания. Очевидно, раз Вы хотите, чтобы я написал Вам новую функцию копирования файла, — вместо того, чтобы воспользоваться одной из множества уже реализованных во всевозможных библиотеках и фреймворках — то у Вас имеются какие-то очень-очень специфические требования, которым библиотечные функции не удовлетворяют, и я намереваюсь эти требования из Вас вытянуть. Конечно, я уже могу быстренько набросать что-нибудь подходящее, но должен отметить, что у нас ещё целая куча не до конца выясненных деталей...
Интервьюер: ААААААААААААААААААААААААААААААААААААААААААА!!!

Самое интересное, что если просто сделать импорт. То вопрос решится.
Если пробовать писать самому(как например сделанов хроме) то выше перечислен перечень потенциальных претензий к коду.

Можете еще обратить Ваше внимание на TUS протокол. tus.io
Есть готовые реализации на разных языках. На PHP, к сожалению, реализация заставляет желать лучшего.

Дякую за посилання. Цікавий протокол та приклади його реалізації. Вже знайшов нові ідеї для себе.

Или используем облачный serverless, где все уже сделано

безпечне != надійне
а загалом подобається...

безпечне != надійне

Мабуть, я не точно виразився. Я мав на увазі, що підвищення надійності завантажувача (алгоритму обміну даними) призводить до підвищення безпеки передачі файлу — зменшення ймовірності отримання пошкодження при транспортуванні. По аналогії, як підвищення надійності транспортного засобу підвищує безпеку самої поїздки на ньому.

а загалом подобається...

Дякую...

Підписатись на коментарі