My Maintenance← На главную
Русский

My Maintenance · 0.6.44 · Windows & Android

Настройка собственной синхронизации Cloudflare

Пошаговая инструкция с иллюстрациями для My Maintenance 0.6.44 в Windows и Android. Облако необязательно; приложение и локальные копии работают без него. На снимках — английский интерфейс Cloudflare без данных учётной записи. Названия приведены для примера.

Шаги
  1. Активируйте R2 и проверьте цены
  2. Создайте закрытый контейнер
  3. Создайте Worker
  4. Установите код My Maintenance
  5. Подключите закрытый контейнер
  6. Создайте и сохраните секретный ключ
  7. Скопируйте адрес Worker
  8. Подключите первое устройство
  9. Выберите состав и синхронизируйте
  10. Подключите и проверьте второе устройство
  11. Храните независимые копии

01 / 11

Активируйте R2 и проверьте цены

Войдите и откройте Storage & databases → R2 Object Storage. При необходимости активируйте R2 и самостоятельно проверьте условия оплаты. Есть ежемесячный бесплатный лимит, но могут потребоваться платёжные данные, а превышение может оплачиваться. Проверьте текущие цены R2 и Workers и настройте уведомления об использовании. My Maintenance бесплатно.

02 / 11

Создайте закрытый контейнер

В R2 выберите Create bucket, например my-maintenance-private. Выберите Standard и подходящий регион или юрисдикцию, затем Create bucket. Не включайте публичный доступ r2.dev и не подключайте публичный домен. Этот же контейнер понадобится на шаге 5.

Создайте закрытый контейнер · Cloudflare English UI

03 / 11

Создайте Worker

Откройте Compute → Workers & Pages → Create application → Start with Hello World! Задайте уникальное имя, например my-maintenance-sync, и нажмите Deploy. Оставьте Protect with Cloudflare Access выключенным: приложение использует собственный ключ. Исходный сервис пока не содержит код синхронизации.

Создайте Worker · Cloudflare English UI
Создайте Worker · Cloudflare English UI

04 / 11

Установите код My Maintenance

Скачайте код Worker ниже. Откройте Worker → Edit code → worker.js. Полностью замените стартовый код содержимым скачанного файла. Не вставляйте ключ в код. Нажмите Deploy. Unauthorized в предпросмотре без ключа — ожидаемый результат.

Показать код
// Deploy into YOUR Cloudflare account. The R2 bucket must remain private.
const STATE='maintenance-state-v1.json';
const TABLES=['object_types','properties','maintenance_plans','spare_parts','maintenance_history','attachments'];
const PURCHASE_TABLES=['purchase_categories','purchased_products','purchase_attachments'];
const STATE_LIMIT=16*1024*1024,FILE_LIMIT=50*1024*1024;
const reply=(message,status=400)=>new Response(message,{status,headers:{'Cache-Control':'no-store','Content-Type':'text/plain; charset=utf-8'}});
async function authorized(request,secret){
 if(typeof secret!=='string'||secret.length<32)return false;
 const supplied=request.headers.get('Authorization')||'';
 if(!supplied.startsWith('Bearer ')||supplied.length>1024)return false;
 const hash=async s=>new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(s)));
 const [a,b]=await Promise.all([hash(supplied.slice(7)),hash(secret)]);let different=0;for(let i=0;i<32;i++)different|=a[i]^b[i];return different===0;
}
async function limitedText(request,limit){
 const reader=request.body?.getReader();if(!reader)return '';
 let total=0;const chunks=[];
 try{while(true){const {done,value}=await reader.read();if(done)break;total+=value.length;if(total>limit){await reader.cancel();throw new Error('too-large')}chunks.push(value);}}finally{reader.releaseLock();}
 const bytes=new Uint8Array(total);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}return new TextDecoder('utf-8',{fatal:true}).decode(bytes);
}
function conditional(headers){
 const match=headers.get('If-Match'),none=headers.get('If-None-Match');
 if(none==='*'&&!match)return true;
 return !!match&&!none&&/^"[^"\r\n]+"$/.test(match);
}
export default {
 async fetch(request,env){
  if(!await authorized(request,env.SYNC_TOKEN))return reply('Unauthorized',401);
  const url=new URL(request.url),name=url.pathname.slice(1),isState=name===STATE;
  const blob=/^blob-([a-f0-9]{64})\.bin$/.exec(name);
  const probe=/^sync-probe-[a-f0-9]{64}\.json$/.test(name);
  if(url.search||(!isState&&!blob&&!probe))return reply('Not found',404);
  if(probe&&request.method==='DELETE'){await env.MAINTENANCE.delete(name);return new Response(null,{status:204});}
  if(!['GET','HEAD','PUT'].includes(request.method))return reply('Method not allowed',405);
  if(request.method==='GET'||request.method==='HEAD'){
   const object=request.method==='HEAD'?await env.MAINTENANCE.head(name):await env.MAINTENANCE.get(name);
   if(!object)return reply('Not found',404);
   const headers=new Headers({'ETag':object.httpEtag,'Content-Length':String(object.size),'Cache-Control':'no-store','Content-Type':isState?'application/json':'application/octet-stream','X-Content-Type-Options':'nosniff'});
   return new Response(request.method==='HEAD'?null:object.body,{headers});
  }
  if(!conditional(request.headers))return reply('A strong If-Match or If-None-Match: * is required',428);
  if(blob&&request.headers.get('If-None-Match')!=='*')return reply('Attachment objects are immutable',428);
  const length=request.headers.get('Content-Length');
  if(length===null||!/^\d+$/.test(length))return reply('Content-Length required',411);
  if(Number(length)>(isState?STATE_LIMIT:probe?1024:FILE_LIMIT))return reply('Too large',413);
  let body=request.body;
  if(isState){
   try {const text=await limitedText(request,STATE_LIMIT),doc=JSON.parse(text);
    const tables=doc.version===2?[...TABLES,...PURCHASE_TABLES]:TABLES;
    if(doc?.format!=='my-maintenance-sync'||![1,2].includes(doc.version)||!Array.isArray(doc.devices)||!doc.tables||Object.keys(doc.tables).sort().join()!==[...tables].sort().join()||tables.some(t=>!Array.isArray(doc.tables[t])))return reply('Invalid maintenance document',422);
    body=text;
   }catch(e){return reply(e.message==='too-large'?'Too large':'Invalid JSON',e.message==='too-large'?413:422);}
  }else if(probe){try{body=await limitedText(request,1024);}catch(_){return reply('Too large',413);}}
  else{
   if(!body)return reply('Attachment body required',400);
   // Keep the original HTTP request body: R2 requires a stream with known length.
   // Cloudflare's HTTP runtime enforces Content-Length; R2 independently verifies SHA-256.
  }
  try {
   const object=await env.MAINTENANCE.put(name,body,{onlyIf:request.headers,httpMetadata:{contentType:isState?'application/json':'application/octet-stream'},...(blob?{sha256:blob[1]}:{})});
   if(!object)return reply('Precondition failed',412);
   return new Response(null,{status:204,headers:{ETag:object.httpEtag,'Cache-Control':'no-store'}});
  } catch (_) {return reply('Storage write failed; no success was acknowledged',502);}
 }
};
Установите код My Maintenance · Cloudflare English UI

05 / 11

Подключите закрытый контейнер

Откройте Settings → Bindings → Add binding. Выберите R2 Bucket → Add Binding. Variable name должно быть точно MAINTENANCE. Выберите закрытый контейнер из шага 2 и Deploy. Если имя уже существует, измените существующую привязку. На снимке тестовый контейнер; выберите свой.

Подключите закрытый контейнер · Cloudflare English UI
Подключите закрытый контейнер · Cloudflare English UI
Пример сохранённых настроек. Выберите свой закрытый бакет. Сохранённый секрет скрыт; значение вводится при его добавлении.

06 / 11

Создайте и сохраните секретный ключ

Создайте случайный стойкий ключ длиной от 32 символов или используйте локальный генератор. Сохраните его в менеджере паролей. Откройте Settings → Runtime variables and secrets → Add variable: Type = Secret, Name = SYNC_TOKEN, Value = ваш ключ. Нажмите Deploy. Затем Cloudflare больше не показывает значение. Это не API-токен учётной записи Cloudflare.

Создайте и сохраните секретный ключ · Cloudflare English UI
Создайте и сохраните секретный ключ · Cloudflare English UI
Пример сохранённых настроек. Выберите свой закрытый бакет. Сохранённый секрет скрыт; значение вводится при его добавлении.

07 / 11

Скопируйте адрес Worker

На странице Overview скопируйте HTTPS-адрес workers.dev, например https://my-maintenance-sync.your-subdomain.workers.dev/. Нужен корневой адрес без пути файла, параметров или адреса панели. Браузер без ключа получает Unauthorized. Не публикуйте ключ вместе с адресом.

08 / 11

Подключите первое устройство

В My Maintenance откройте Настройки → Необязательная облачная синхронизация и выберите Cloudflare. Введите HTTPS-адрес и ключ SYNC_TOKEN. Отметьте согласие и сохраните подключение; дождитесь подтверждения. Проверьте подключение. Сохранение и проверка ещё не объединяют записи; синхронизация проверяет также безопасную запись.

Подключите первое устройство · Cloudflare English UI

09 / 11

Выберите состав и синхронизируйте

Выберите только обслуживание или добавьте «Где куплено», чтобы включить оба модуля, категории, товары, фото и документы. Запустите синхронизацию и дождитесь результата. При конфликтах сравните стороны и выберите нужные записи. Удаления тоже передаются. Напоминания о местах, разрешения, расписание копий и ключи остаются на своём устройстве.

10 / 11

Подключите и проверьте второе устройство

Настройте тот же адрес и ключ на втором устройстве. Выберите тот же состав и синхронизируйте. Если изменения были на обоих, повторите синхронизацию первого. Проверьте на обоих объект, план, товар, фото и документ. Только обслуживание не меняет покупки. Используйте актуальные совместимые версии.

11 / 11

Храните независимые копии

Синхронизация не заменяет резервную копию: удаление может попасть на все устройства. Храните локальные копии и ещё одну безопасную копию отдельно. Отключение облака сохраняет локальные данные. Удаление подключения убирает ключ с устройства, но не файлы в облаке. При утечке смените SYNC_TOKEN и обновите все устройства.

Устранение неполадок

Unauthorized / 401

Ключ приложения должен точно совпадать с SYNC_TOKEN, без лишних пробелов. Тип — Secret; опубликуйте через Deploy. В браузере без ключа Unauthorized нормально.

Нет привязки или ошибка сервера

Проверьте MAINTENANCE, закрытый контейнер, полный код и последнее развёртывание. База D1 не нужна.

Подключение работает, синхронизация нет

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

Нет фото или документа

Для вложений покупок включите «Где куплено». Откройте оригинал на отправителе. Синхронизация не восстановит отсутствующий локальный файл. Не удаляйте всю запись из-за одного файла.

Официальные инструкции и актуальные цены

Снимки панели Cloudflare от 25–26 октября 2026 года. Если интерфейс изменится, следуйте названиям полей и значениям в инструкции.

Руководство пользователя →