InquirerNo flowers, no visitors: The forgotten graves of UndasThe Jerusalem PostNetanyahu survives the election, but Eisenkot holds key to replacing him - analysisPunchHow Hamzat stepped aside for me in 2018 — Sanwo-OluCNN TürkBeşiktaş, Virtus Bologna deplasmanında kazandıInquirer EntertainmentMargaret Qualley, Jack Antonoff file for divorce after 3 years of marriageBollywood HungamaFilmmaker Sanjay Gupta passes away; director leaves behind a distinctive crime cinema legacyZDF heuteAktuelle Pressemitteilungen des ZDFUOLTiroteio em loja de Halloween deixa um morto e seis feridos na CalifórniaSouth China Morning PostBoth Japan and China can help Bangladesh reach its full potentialRapplerThe races that will decide control of the US SenateSözcüEverest'te facia: Otel alev alev yandıIl Fatto Quotidiano“Ha ucciso tre parenti servendo loro un filetto con funghi velenosi. Crimini atroci, commessi con una freddezza straordinaria”: Erin Patterson condannata a trascorrere tutta la vita in carcere
The Daily Newsstand · Free, Always
Saturday, October 31, 2026

Копируем содержимое Яндекс.Диск к себе

Translate
  • есть Яндекс.Диск объемом ~500GB, заполненный в основном фото (RAW и JPEG) и видео

  • большинство файлов успешно скачивается через web-интерфейс на полной скорости

  • есть задача сохранить все его содержимое на локальный диск, насколько это возможно, учитывая последние события

Вроде бы ровно для этого и нужен официальный клиент - но в реальных условиях он повел себя крайне странно:

  • не позволил выбрать целевую папку ни на каком локальном диске, кроме C:

  • несмотря на позитивные новости, регулярно натыкался на какой-то недоступный файл и впадал в ступор

  • скорость загрузки оставляла желать лучшего

Пришлось брать дело в свои руки, а заодно плотнее познакомиться с node:streams...

Как все устроено "для браузера"

Пробовать API Диска после попыток использовать "родной" клиент не хотелось совсем - лучше посмотрим, как работает web-клиент в браузере, и постараемся прикинуться им.

В общем-то от нас никто ничего не скрывает, и нам потребуется реализовать всего несколько типов операций.

Листинг раздела

Чтобы получить список объектов любого раздела, включая корневой, используется POST-запрос по адресу https://disk.yandex.ru/models-v2?m=mpfs/resources.

Аргументы:

{
  sort       : 'name'  // сортировка по имени
, order      : '1'
, idContext  : '/disk' // path до объекта
, amount     : 40      // количество элементов на странице
, offset     : 0       // стартовый по порядку элемент
}

Web-клиент запрашивает списки файлов страницами по 40 штук, и мы не будем нарушать эту традицию.

В ответ получаем JSON-массив примерно таких объектов с вполне прозрачной структурой:

{
  type  : "dir"              // тип объекта: 'dir' или 'file'
, id    : "/disk/My Folder/" // полный путь к объекту на Диске
, name  : "My Folder"        // имя объекта
, path  : "/disk/My Folder"  // полный путь к объекту на Диске, еще раз
, meta  : {
    size : 123456            // размер - для файла
  }
  // время создания, изменения, ... в Unix Timestamp
, ctime : ...
, mtime : ...
, utime : ...
}

Путь к файлу

Еще один POST-запрос, теперь по адресу https://disk.yandex.ru/models-v2?m=mpfs/url с единственным аргументом path помогает нам получить URL для загрузки файла:

{
  file : "//downloader.disk.yandex.ru/disk/..."
}

Тело файла

Обратившись к полученному URL, теперь уже GET-запросом, мы получим HTTP 302-редирект с адресом конкретного хранилища в заголовке:

location : https://mds.storage.yandex.net/rdisk/...

Собственно, там и находится искомый контент.

Загрузчик

Для того, чтобы "притворяться браузером" условно-корректно, нам потребуются заголовки любого из упомянутых выше POST-запроса - их легко скопировать прямо из браузера:

Нам потребуются сами заголовки (headers) и два параметра, передаваемых в body: sk и connection_id.

Данные сессии (sk / connection_id) "протухают" примерно через сутки после получения. Поэтому если контента у вас много - загрузка отвалится, и необходимо будет взять из браузера актуальные реквизиты.

Я постарался обойтись минимумом внешних библиотек. Понадобилась всего одна - utimes для удобной простановки атрибутов времени на файлах и каталогах под Windows.

Основной алгоритм весьма прост:

  • обрабатываем очередь задач tasks, пока она не опустеет, начиная с "корня" /disk (если вам более приоритетна конкретная папка - подставьте ее)

  • для задачи-каталога (type == 'dir') страницами по 40 получаем все лежащие внутри элементы и добавляем в начало очереди задач

  • для задачи-файла (type == 'file') пытаемся несколько раз получить его контент, и если загруженный размер совпадает с ожидаемым - выкладываем в целевое хранилище

  • в некоторых случаях при доступе к файлу случаются таймауты, HTTP 500 или просто обрыв в ходе загрузки - поэтому делаем пару попыток, пока не удалось загрузить весь ожидаемый объем файла

В моем случае целевой диск находится на медленном сетевом хранилище, поэтому тело файла сначала потоково "чанками" пишется во временный файл на локальном диске, а потом целиком копируется куда надо.

// yad-dl.js

// очередь задач
const tasks = [
  {
    id     : '/disk/'
  , type   : 'dir'
  , path   : '/disk' // откуда начинаем сканирование
  , name   : 'disk'
  , depth  : 0
  , target : 'Z:\\#ya.dsk.dump' // целевая папка на локальном диске
  }
];

// реквизиты сессии
const headers = {
  ...,
  "Referer": "https://disk.yandex.ru/client/disk"
};
const reqJSON = {
  sk            : '...:...'
, connection_id : '...'
};

const path = require('node:path');
const fs = require('node:fs');
const {Readable, Transform} = require('node:stream');
const {pipeline} = require('node:stream/promises');

const {utimes} = require('utimes');

// форматируем таймстамп в убодное представление
Date.prototype.toTimeStringEx = function() {
  return `${this.getFullYear()}-${(this.getMonth() + 1).toString().padStart(2, '0')}-${this.getDate().toString().padStart(2, '0')} ${this.getHours().toString().padStart(2, '0')}:${this.getMinutes().toString().padStart(2, '0')}:${this.getSeconds().toString().padStart(2, '0')}.${this.getMilliseconds().toString().padStart(3, '0')}`;
};

// статистика обработки
const filestat = {
  skip  : 0
, dump  : 0
, error : 0
};

// создаем временную папку
const temp = fs.mkdtempSync('yad-dl');

(async () => {
  // обрабатываем задачи из очереди по обной
  do {
    const task = tasks.shift();
    if (!task) {
      break;
    }
    task.name = task.name.replaceAll(/[\:\?\"]/g, '+'); // убираем недопустимые символы из имени
    task.target = path.join(task.target, task.name);
    console.log(`${new Date().toTimeStringEx()} | -- ${task.type == 'file' ? 'f' : 'd'} : ${JSON.stringify(task.path)}`);
    console.log(`${new Date().toTimeStringEx()} | -- --- ${JSON.stringify(task.target)}`);

    // обработка папки
    if (task.type == 'dir') {
      // если целевой папки еще нет - создаем ее и проставляем ей атрибуты времени
      if (!fs.existsSync(task.target)) {
        try {
          fs.mkdirSync(task.target);
        }
        catch (e) {
          console.log(`${new Date().toTimeStringEx()} | mkdir`, e);
        }
        await utimes(task.target, {
          btime : (task.ctime ?? 0) * 1000
        , mtime : (task.mtime ?? 0) * 1000
        , atime : (task.utime ?? 0) * 1000
        });
      }
      else {
        console.log(`${new Date().toTimeStringEx()} | -- -- : skip mkdir`);
      }

      // загружаем вложенные элементы "страницами" по 40 штук
      const nested = [];
      const amount = 40;
      do {
        console.log(`${new Date().toTimeStringEx()} | -- -- : get offset : ${nested.length.toString().padStart(9)}`);
        const res = await fetch(
          'https://disk.yandex.ru/models-v2?m=mpfs/resources'
        , {
            headers
          , method : 'POST'
          , body   : JSON.stringify({
              ...reqJSON
              , apiMethod     : 'mpfs/resources'
              , requestParams : {
                  sort       : 'name'
                , order      : '1'
                , idContext  : task.path
                , amount
                , offset     : nested.length
                , with_share : '1'
                }
            })
          }
        );
        const {resources} = await res.json();
        console.log(`${new Date().toTimeStringEx()} | -- -- : segment-sz : ${resources.length.toString().padStart(9)}`);
        nested.push(...resources);
        if (resources.length < amount) {
          break;
        }
      }
      while (true);
      // прописываем всем "дочерним" задачам целевой каталог и добавляем в начало очереди
      nested.forEach(item => item.target = task.target);
      tasks.unshift(...nested);
    }
    // обработка файла
    if (task.type == 'file') {
      console.log(`${new Date().toTimeStringEx()} | -- -- : vol = ${task.meta.size.toString().padStart(10)}`);
      // пропускаем, если целевой файл уже есть, и нужного размера
      let need = !fs.existsSync(task.target);
      if (!need) {
        const {size} = fs.statSync(task.target);
        need ||= size != task.meta.size;
        if (need) {
          fs.rmSync(task.target); // если размер не совпал - зачищаем
        }
        else {
          console.log(`${new Date().toTimeStringEx()} | -- -- : skip file`);
          filestat.skip++;
        }
      }
      if (need) {
        console.log(`${new Date().toTimeStringEx()} | -- -- : get storage : ${JSON.stringify(task.path)}`);
        // получаем URL для доступа к файлу
        const res = await fetch(
          'https://disk.yandex.ru/models-v2?m=mpfs/url'
        , {
            headers
          , method : 'POST'
          , body   : JSON.stringify({
              ...reqJSON
            , apiMethod     : 'mpfs/url'
            , requestParams : {
                path : task.path
              }
            })
          }
        );
        const data = await res.json();
        if (data.file) {
          let retry = 2; // до 2 попыток загрузки
          let success;
          do {
            console.log(`${new Date().toTimeStringEx()} | -- -- : rcv storage : ${data.file}`);
            const res = await fetch('https:' + data.file, {
              headers
            , method : 'GET'
            })
              .catch(e => {
                console.log(`${new Date().toTimeStringEx()} | GET`, e);
              });
            if (res?.status == 200) {
              const tempfile = path.join(temp, task.name);
              console.log(`${new Date().toTimeStringEx()} | -- -- : rcv data | ${tempfile}`);
              // считаем полученный объем файла и выводим в лог раз в секунду
              const tsb = Date.now();
              let size = 0;
              let tsl = tsb;
              const monitor = new Transform({
                transform(chunk, encoding, callback) {
                  size += chunk.length;
                  const tse = Date.now();
                  if (tse >= tsl + 1000 || size == task.meta.size) {
                    console.log(`${new Date().toTimeStringEx()} | -- -- : rcv = ${size.toString().padStart(10)} (${(size/task.meta.size * 100).toFixed(2).padStart(6)}%) | ${(size/((tse - tsb)/1000)/1024/1024).toFixed(2).padStart(6)} MB/s | ETA = ${new Date(tse + (tse - tsb)/size * (task.meta.size - size)).toTimeStringEx()}`);
                    tsl = tse;
                  }
                  this.push(chunk);
                  callback();
                }
              });

              try {
                // перенаправляем поток загружаемого файла через наш монитор во временный файл
                await pipeline(
                  Readable.fromWeb(res.body)
                   .on('error', e => {
                     console.log(`${new Date().toTimeStringEx()} | bodyStream`, e);
                   })
                , monitor
                , fs.createWriteStream(tempfile, {flags : 'wx'})
                )
                  .catch(e => {
                    console.log(`${new Date().toTimeStringEx()} | finished`, e);
                  });
                // если загруженный размер совпадает с ожидаемым - перекопируем в целевое хранилище
                if (size == task.meta.size) {
                  console.log(`${new Date().toTimeStringEx()} | -- -- : cpy = ${task.target}`);
                  fs.copyFileSync(tempfile, task.target);
                  await utimes(task.target, {
                    btime : task.ctime * 1000
                  , mtime : task.mtime * 1000
                  , atime : task.utime * 1000
                  });
                  const tse = Date.now();
                  console.log(`${new Date().toTimeStringEx()} | -- -- : mov = ${size.toString().padStart(10)} (${(size/task.meta.size * 100).toFixed(2).padStart(6)}%) | ${(size/((tse - tsl)/1000)/1024/1024).toFixed(2).padStart(6)} MB/s |`);
                  success = true;
                  filestat.dump++;
                }
                else {
                  console.log(`${new Date().toTimeStringEx()} | -- -- : neq sizes : ${size} != ${task.meta.size}`);
                }
              }
              catch (e) {
                console.log(`${new Date().toTimeStringEx()} | catch`, e);
              }
              fs.rmSync(tempfile);
            }
            else {
              console.log(`${new Date().toTimeStringEx()} | -- -- : ${res?.status} - ${res?.statusText}`);
            }
          }
          while (!success && --retry);
          if (!success) {
            // файл, который не удалось получить, выводится в STDERR
            console.error(`${new Date().toTimeStringEx()} | ${task.path}`);
            filestat.error++;
          }
        }
        else {
          console.log(data);
        }
      }
    }
  } while (true);
  console.log(`${new Date().toTimeStringEx()} | finish | skip : ${filestat.skip}, dump : ${filestat.dump}, error : ${filestat.error}`);
})();

Поскольку часть файлов все еще недоступна, но постепенно "открывается", рекомендуется периодически перезапускать скрипт. Я для себя сделал следующий bat-файл, который формирует новую пару log/err-файлов при каждом запуске, чтобы можно было сразу увидеть, что еще не загрузилось в этот раз:

@echo off
for /f "tokens=2 delims==" %%i in ('wmic OS Get localdatetime /value') do set "dt=%%i"
set "timestamp=%dt:~0,14%"
node yad-dl.js >_log-%timestamp% 2>_err-%timestamp%

Вместо заключения

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

Возможно, вам будет достаточно и официального клиента, но у меня он так и не смог показать утилизацию канала выше 1Mbps. Эксперименты же со скриптом показали, что сейчас файлы отдаются хоть и на разных уровнях скорости, зато от 1.5Mbps до 50Mbps (видимо, в зависимости от конкретного ЦОД).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.