@interware/content-generator 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/_chunks/{App-h4_txTsI.mjs → App-DD1MzaWm.mjs} +2 -2
  2. package/dist/_chunks/{App-Cwr5ra5m.js → App-DspaMfyW.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-B-mOwDfY.mjs → ImageStudioTabs-DwaLxRjl.mjs} +367 -61
  4. package/dist/_chunks/{ImageStudioTabs-CG4KTn1S.js → ImageStudioTabs-uXSBkFeu.js} +365 -59
  5. package/dist/_chunks/{Settings-BT-EqvXs.mjs → Settings-C6TbUrgU.mjs} +170 -2
  6. package/dist/_chunks/{Settings-D4NGBTk1.js → Settings-ps0o7-fC.js} +169 -1
  7. package/dist/_chunks/{StudioApp-xaEy-uIk.mjs → StudioApp-B5I1OfGw.mjs} +2 -2
  8. package/dist/_chunks/{StudioApp-_ViSAwCm.js → StudioApp-B7kGpqqo.js} +2 -2
  9. package/dist/_chunks/{index-NA9OuMGs.js → index-BudahdfX.js} +3 -3
  10. package/dist/_chunks/{index-D67xWUPz.mjs → index-zenHkX-p.mjs} +3 -3
  11. package/dist/admin/index.js +1 -1
  12. package/dist/admin/index.mjs +1 -1
  13. package/dist/admin/src/components/settings/DriveSettings.d.ts +77 -0
  14. package/dist/admin/src/features/drive/DriveTab.d.ts +42 -0
  15. package/dist/admin/src/features/image-studio/ImageStudioTabs.d.ts +3 -0
  16. package/dist/admin/src/features/library/FileCard.d.ts +5 -2
  17. package/dist/admin/src/features/library/driveSync.d.ts +41 -0
  18. package/dist/admin/src/features/templates/editor/ColorField.d.ts +4 -2
  19. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  20. package/dist/admin/src/features/templates/model/palette.d.ts +2 -2
  21. package/dist/admin/src/lib/imageLimits.d.ts +9 -0
  22. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  23. package/dist/server/index.js +1414 -51
  24. package/dist/server/index.mjs +1412 -49
  25. package/dist/server/src/bootstrap.d.ts +2 -0
  26. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  27. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  28. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  29. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  30. package/dist/server/src/content-types/index.d.ts +106 -0
  31. package/dist/server/src/controllers/drive.d.ts +30 -0
  32. package/dist/server/src/controllers/index.d.ts +130 -0
  33. package/dist/server/src/drive/client.d.ts +32 -0
  34. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  35. package/dist/server/src/drive/common.d.ts +6 -0
  36. package/dist/server/src/drive/fakes.d.ts +104 -0
  37. package/dist/server/src/drive/files.d.ts +136 -0
  38. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  39. package/dist/server/src/drive/links.d.ts +23 -0
  40. package/dist/server/src/drive/model.d.ts +99 -0
  41. package/dist/server/src/drive/oauth.d.ts +47 -0
  42. package/dist/server/src/drive/pull.d.ts +23 -0
  43. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  44. package/dist/server/src/drive/sync.d.ts +40 -0
  45. package/dist/server/src/index.d.ts +294 -0
  46. package/dist/server/src/services/drive.d.ts +141 -0
  47. package/dist/server/src/services/index.d.ts +55 -0
  48. package/package.json +1 -1
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  const fs = require("fs");
3
3
  const genai = require("@google/genai");
4
+ const node_crypto = require("node:crypto");
5
+ const promises = require("node:fs/promises");
6
+ const node_os = require("node:os");
7
+ const node_path = require("node:path");
8
+ const node_async_hooks = require("node:async_hooks");
4
9
  function _interopNamespace(e) {
5
10
  if (e && e.__esModule) return e;
6
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -20,10 +25,10 @@ function _interopNamespace(e) {
20
25
  }
21
26
  const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
22
27
  const PLUGIN_ID = "strapi-content-generator-gemini";
23
- const FOLDER_UID = "plugin::upload.folder";
28
+ const FOLDER_UID$1 = "plugin::upload.folder";
24
29
  const hasValidPath = (path) => /^(\/\d+)+$/.test(path);
25
30
  const repairFolderPaths = async (strapi, name2) => {
26
- const query = strapi.db.query(FOLDER_UID);
31
+ const query = strapi.db.query(FOLDER_UID$1);
27
32
  const folders = await query.findMany({ where: { name: name2 }, populate: ["parent"] });
28
33
  const repaired = [];
29
34
  for (const f of folders) {
@@ -35,13 +40,34 @@ const repairFolderPaths = async (strapi, name2) => {
35
40
  return repaired;
36
41
  };
37
42
  const ensureFolder = async (strapi, name2) => {
38
- const folders = await strapi.db.query(FOLDER_UID).findMany({ where: { name: name2 }, populate: ["parent"] });
43
+ const folders = await strapi.db.query(FOLDER_UID$1).findMany({ where: { name: name2 }, populate: ["parent"] });
39
44
  const atRoot = folders.find((f) => !f.parent);
40
45
  if (atRoot) return atRoot.id;
41
46
  const created = await strapi.plugin("upload").service("folder").create({ name: name2, parent: null });
42
47
  return created.id;
43
48
  };
49
+ const MODELS = { "plugin::upload.file": "file", "plugin::upload.folder": "folder" };
50
+ const subscribeUploadLifecycles = (strapi, onChange) => {
51
+ const notify = (event, id) => {
52
+ const cmsType = MODELS[event.model.uid];
53
+ if (!cmsType || typeof id !== "number") return;
54
+ onChange(cmsType, id).catch((err) => strapi.log.warn(`[drive] no se pudo encolar ${cmsType} ${id}: ${err.message}`));
55
+ };
56
+ strapi.db.lifecycles.subscribe({
57
+ models: Object.keys(MODELS),
58
+ afterCreate: (event) => notify(event, event.result?.id),
59
+ afterUpdate: (event) => notify(event, event.result?.id),
60
+ afterDelete: (event) => notify(event, event.result?.id),
61
+ // `deleteMany` solo trae el `where`: los ids se leen antes de borrar.
62
+ async beforeDeleteMany(event) {
63
+ const rows = await strapi.db.query(event.model.uid).findMany({ where: event.params?.where, select: ["id"] });
64
+ if (event.state) event.state.driveIds = rows.map((r) => r.id);
65
+ },
66
+ afterDeleteMany: (event) => (event.state?.driveIds ?? []).forEach((id) => notify(event, id))
67
+ });
68
+ };
44
69
  const SCHEDULE_CRON = "content-generator-publicar-programados";
70
+ const DRIVE_CRON = "content-generator-drive";
45
71
  const bootstrap = async ({ strapi }) => {
46
72
  strapi.cron.add({
47
73
  [SCHEDULE_CRON]: {
@@ -55,6 +81,19 @@ const bootstrap = async ({ strapi }) => {
55
81
  options: { rule: "* * * * *" }
56
82
  }
57
83
  });
84
+ strapi.cron.add({
85
+ [DRIVE_CRON]: {
86
+ task: async () => {
87
+ try {
88
+ await strapi.plugin(PLUGIN_ID).service("drive").runCycle();
89
+ } catch (err) {
90
+ strapi.log.error(`[content-generator] sincronizar con Drive: ${err.message}`);
91
+ }
92
+ },
93
+ options: { rule: "*/15 * * * *" }
94
+ }
95
+ });
96
+ subscribeUploadLifecycles(strapi, (cmsType, cmsId) => strapi.plugin(PLUGIN_ID).service("drive").track(cmsType, cmsId));
58
97
  strapi.admin.services.permission.actionProvider.registerMany([
59
98
  {
60
99
  section: "plugins",
@@ -80,7 +119,7 @@ const config$2 = {
80
119
  validator() {
81
120
  }
82
121
  };
83
- const schema = {
122
+ const schema$2 = {
84
123
  kind: "collectionType",
85
124
  collectionName: "iw_image_templates",
86
125
  info: {
@@ -109,9 +148,67 @@ const schema = {
109
148
  replacesBase: { type: "string" }
110
149
  }
111
150
  };
112
- const template = { schema };
151
+ const template = { schema: schema$2 };
152
+ const schema$1 = {
153
+ kind: "collectionType",
154
+ collectionName: "iw_drive_links",
155
+ info: {
156
+ singularName: "drive-link",
157
+ pluralName: "drive-links",
158
+ displayName: "Vínculo con Drive",
159
+ description: "Sincronización con Google Drive (content-generator)"
160
+ },
161
+ options: { draftAndPublish: false },
162
+ pluginOptions: {
163
+ "content-manager": { visible: false },
164
+ "content-type-builder": { visible: false }
165
+ },
166
+ attributes: {
167
+ cmsType: { type: "enumeration", enum: ["file", "folder"], required: true },
168
+ cmsId: { type: "integer", required: true },
169
+ /** Vacío mientras el archivo espera su primera subida. */
170
+ driveId: { type: "string" },
171
+ /** Lo marcó el usuario; los demás vínculos son contenido o ancestros de algo marcado. */
172
+ root: { type: "boolean", default: false },
173
+ name: { type: "string" },
174
+ parentCmsId: { type: "integer" },
175
+ /** Archivo: `url|size` de la última subida. */
176
+ version: { type: "string" },
177
+ /** `md5Checksum` de Drive de la última versión sincronizada (S7.4). */
178
+ driveRev: { type: "string" },
179
+ status: { type: "enumeration", enum: ["synced", "pending", "error", "skipped"], default: "pending" },
180
+ error: { type: "text" }
181
+ }
182
+ };
183
+ const driveLink = { schema: schema$1 };
184
+ const schema = {
185
+ kind: "collectionType",
186
+ collectionName: "iw_drive_ops",
187
+ info: {
188
+ singularName: "drive-op",
189
+ pluralName: "drive-ops",
190
+ displayName: "Operación de Drive",
191
+ description: "Cola de la sincronización con Google Drive (content-generator)"
192
+ },
193
+ options: { draftAndPublish: false },
194
+ pluginOptions: {
195
+ "content-manager": { visible: false },
196
+ "content-type-builder": { visible: false }
197
+ },
198
+ attributes: {
199
+ cmsType: { type: "enumeration", enum: ["file", "folder"], required: true },
200
+ cmsId: { type: "integer", required: true },
201
+ attempts: { type: "integer", default: 0 },
202
+ /** Epoch en ms: no se procesa antes (espera creciente tras un error). */
203
+ runAfter: { type: "biginteger", default: "0" },
204
+ error: { type: "text" }
205
+ }
206
+ };
207
+ const driveOp = { schema };
113
208
  const contentTypes = {
114
- template
209
+ template,
210
+ "drive-link": driveLink,
211
+ "drive-op": driveOp
115
212
  };
116
213
  const POST_MAX = 3e3;
117
214
  const hashtags = (keywords) => keywords.map((k) => k.trim().replace(/\s+/g, "").replace(/[^\p{L}\p{N}_]/gu, "")).filter(Boolean).map((k) => `#${k}`).join(" ");
@@ -428,10 +525,11 @@ const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
428
525
  const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
429
526
  return { models: ai.models, backend };
430
527
  });
528
+ const IMAGE_MAX_MB = 30;
529
+ const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
431
530
  class InputError extends Error {
432
531
  }
433
532
  const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
434
- const MAX_BYTES = 10 * 1024 * 1024;
435
533
  const str = (v) => typeof v === "string" ? v : "";
436
534
  const bool = (v) => v === true || v === "true";
437
535
  const list = (v) => v ? Array.isArray(v) ? v : [v] : [];
@@ -442,7 +540,7 @@ const clean = (text2, labels) => {
442
540
  };
443
541
  const checkImage = (f) => {
444
542
  if (!ALLOWED.includes(f.mimetype ?? "")) throw new InputError("Las imágenes deben ser PNG, JPG o WEBP.");
445
- if (f.size > MAX_BYTES) throw new InputError("Cada imagen puede pesar hasta 10 MB.");
543
+ if (f.size > IMAGE_MAX_BYTES) throw new InputError(`Cada imagen puede pesar hasta ${IMAGE_MAX_MB} MB.`);
446
544
  return f;
447
545
  };
448
546
  const parseGenerateInput = (fields, files) => {
@@ -485,7 +583,7 @@ const parseEditInput = (fields, files) => {
485
583
  aspectRatio: model.aspectRatios.includes(str(fields.aspectRatio)) ? str(fields.aspectRatio) : void 0
486
584
  };
487
585
  };
488
- const fail$3 = (ctx, status, name2, message) => {
586
+ const fail$4 = (ctx, status, name2, message) => {
489
587
  ctx.status = status;
490
588
  ctx.body = { data: null, error: { status, name: name2, message } };
491
589
  };
@@ -495,13 +593,13 @@ const imageStudio = ({ strapi }) => {
495
593
  try {
496
594
  ctx.body = await work();
497
595
  } catch (err) {
498
- if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
596
+ if (err instanceof InputError) return fail$4(ctx, 400, "BadRequestError", err.message);
499
597
  if (err instanceof BlockedError) {
500
598
  strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
501
- return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
599
+ return fail$4(ctx, 422, "UnprocessableEntityError", err.message);
502
600
  }
503
601
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
504
- return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
602
+ return fail$4(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
505
603
  }
506
604
  };
507
605
  return {
@@ -710,11 +808,11 @@ const ARTICLE_SCHEMA = {
710
808
  const ZONA = "America/Mexico_City";
711
809
  const UN_ANIO_MS = 365 * 24 * 60 * 60 * 1e3;
712
810
  const fechaEnMexico = (date) => new Intl.DateTimeFormat("en-CA", { timeZone: ZONA, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
713
- const parseScheduleAt = (input, now) => {
811
+ const parseScheduleAt = (input, now2) => {
714
812
  const date = typeof input === "string" ? new Date(input) : /* @__PURE__ */ new Date(NaN);
715
813
  if (Number.isNaN(date.getTime())) throw new InputError("La fecha y hora no son válidas.");
716
- if (date.getTime() <= now.getTime()) throw new InputError("Elige una fecha y hora futuras.");
717
- if (date.getTime() - now.getTime() > UN_ANIO_MS) throw new InputError("Programa con menos de un año de anticipación.");
814
+ if (date.getTime() <= now2.getTime()) throw new InputError("Elige una fecha y hora futuras.");
815
+ if (date.getTime() - now2.getTime() > UN_ANIO_MS) throw new InputError("Programa con menos de un año de anticipación.");
718
816
  return date;
719
817
  };
720
818
  const estadoDe = ({ publicado, programada }) => publicado ? "publicado" : programada ? "programado" : "borrador";
@@ -760,7 +858,7 @@ const toDetail = (d, publicado) => ({
760
858
  keywords: (d.keywords ?? []).map((k) => k.name ?? "").filter(Boolean),
761
859
  fuentes: Array.isArray(d.fuentes) ? d.fuentes : []
762
860
  });
763
- const createArticles = ({ articulos, categorias, getModels, now = () => /* @__PURE__ */ new Date(), log = silentLog, promptStore = memoryStore() }) => {
861
+ const createArticles = ({ articulos, categorias, getModels, now: now2 = () => /* @__PURE__ */ new Date(), log = silentLog, promptStore = memoryStore() }) => {
764
862
  const draftOf = async (documentId, populate = DETAIL_POPULATE) => {
765
863
  const d = await articulos.findOne({ documentId, status: "draft", populate });
766
864
  if (!d) throw new NotFoundError("El artículo no existe.");
@@ -864,12 +962,12 @@ const createArticles = ({ articulos, categorias, getModels, now = () => /* @__PU
864
962
  async publish(documentId) {
865
963
  const d = await draftOf(documentId);
866
964
  if (!d.image) throw new InputError("Elige una imagen antes de publicar.");
867
- await articulos.update({ documentId, data: { fecha_publicacion: fechaEnMexico(now()), publicacion_programada: null } });
965
+ await articulos.update({ documentId, data: { fecha_publicacion: fechaEnMexico(now2()), publicacion_programada: null } });
868
966
  await articulos.publish({ documentId });
869
967
  return detail(documentId);
870
968
  },
871
969
  async schedule(documentId, at) {
872
- const date = parseScheduleAt(at, now());
970
+ const date = parseScheduleAt(at, now2());
873
971
  const d = await draftOf(documentId);
874
972
  if (!d.image) throw new InputError("Elige una imagen antes de programar.");
875
973
  if (await isPublished(documentId)) throw new InputError("El artículo ya está publicado.");
@@ -885,7 +983,7 @@ const createArticles = ({ articulos, categorias, getModels, now = () => /* @__PU
885
983
  async publishDue() {
886
984
  const due = await articulos.findMany({
887
985
  status: "draft",
888
- filters: { publicacion_programada: { $notNull: true, $lte: now().toISOString() } },
986
+ filters: { publicacion_programada: { $notNull: true, $lte: now2().toISOString() } },
889
987
  fields: ["publicacion_programada"],
890
988
  populate: { image: { fields: ["id"] } }
891
989
  });
@@ -927,7 +1025,7 @@ const articles$1 = ({ strapi }) => createArticles({
927
1025
  };
928
1026
  })()
929
1027
  });
930
- const fail$2 = (ctx, status, name2, message) => {
1028
+ const fail$3 = (ctx, status, name2, message) => {
931
1029
  ctx.status = status;
932
1030
  ctx.body = { data: null, error: { status, name: name2, message } };
933
1031
  };
@@ -938,11 +1036,11 @@ const articles = ({ strapi }) => {
938
1036
  try {
939
1037
  ctx.body = await work();
940
1038
  } catch (err) {
941
- if (err instanceof InputError) return fail$2(ctx, 400, "BadRequestError", err.message);
942
- if (err instanceof NotFoundError) return fail$2(ctx, 404, "NotFoundError", err.message);
943
- if (err instanceof Error && err.name === "ValidationError") return fail$2(ctx, 400, "ValidationError", err.message);
1039
+ if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
1040
+ if (err instanceof NotFoundError) return fail$3(ctx, 404, "NotFoundError", err.message);
1041
+ if (err instanceof Error && err.name === "ValidationError") return fail$3(ctx, 400, "ValidationError", err.message);
944
1042
  strapi.log.error(`[articles] ${err instanceof Error ? err.message : String(err)}`);
945
- return fail$2(ctx, 502, "BadGatewayError", "El servicio de generación no respondió. Intenta de nuevo en un momento.");
1043
+ return fail$3(ctx, 502, "BadGatewayError", "El servicio de generación no respondió. Intenta de nuevo en un momento.");
946
1044
  }
947
1045
  };
948
1046
  return {
@@ -1108,7 +1206,7 @@ class TemplateInputError extends Error {
1108
1206
  }
1109
1207
  const LAYER_TYPES = ["text", "list", "quote", "rect", "shape", "badge", "logo", "image", "icon", "avatar", "qr"];
1110
1208
  const MAX_LAYERS = 200;
1111
- const fail$1 = (message) => {
1209
+ const fail$2 = (message) => {
1112
1210
  throw new TemplateInputError(message);
1113
1211
  };
1114
1212
  const hasDataUrl = (value) => {
@@ -1119,26 +1217,26 @@ const hasDataUrl = (value) => {
1119
1217
  };
1120
1218
  const name = (v) => {
1121
1219
  const n = typeof v === "string" ? v.trim() : "";
1122
- if (!n || n.length > 120) fail$1("El nombre debe tener entre 1 y 120 caracteres.");
1220
+ if (!n || n.length > 120) fail$2("El nombre debe tener entre 1 y 120 caracteres.");
1123
1221
  return n;
1124
1222
  };
1125
1223
  const size = (v) => {
1126
- if (!Number.isInteger(v) || v < 100 || v > 4e3) fail$1("Las medidas deben ser enteros de 100 a 4000 px.");
1224
+ if (!Number.isInteger(v) || v < 100 || v > 4e3) fail$2("Las medidas deben ser enteros de 100 a 4000 px.");
1127
1225
  return v;
1128
1226
  };
1129
1227
  const nonNegative = (v, label) => {
1130
- if (typeof v !== "number" || !Number.isFinite(v) || v < 0) fail$1(`${label} debe ser un número positivo.`);
1228
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) fail$2(`${label} debe ser un número positivo.`);
1131
1229
  return v;
1132
1230
  };
1133
1231
  const layers = (v) => {
1134
- if (!Array.isArray(v) || v.length > MAX_LAYERS) fail$1(`Las capas deben ser una lista de hasta ${MAX_LAYERS}.`);
1232
+ if (!Array.isArray(v) || v.length > MAX_LAYERS) fail$2(`Las capas deben ser una lista de hasta ${MAX_LAYERS}.`);
1135
1233
  v.forEach((l, i) => {
1136
1234
  const layer = l ?? {};
1137
- if (typeof layer.id !== "string" || !layer.id) fail$1(`La capa ${i + 1} no tiene id.`);
1138
- if (!LAYER_TYPES.includes(layer.type)) fail$1(`La capa ${i + 1} tiene un tipo desconocido.`);
1139
- if (typeof layer.x !== "number" || typeof layer.y !== "number") fail$1(`La capa ${i + 1} no tiene posición.`);
1235
+ if (typeof layer.id !== "string" || !layer.id) fail$2(`La capa ${i + 1} no tiene id.`);
1236
+ if (!LAYER_TYPES.includes(layer.type)) fail$2(`La capa ${i + 1} tiene un tipo desconocido.`);
1237
+ if (typeof layer.x !== "number" || typeof layer.y !== "number") fail$2(`La capa ${i + 1} no tiene posición.`);
1140
1238
  });
1141
- if (hasDataUrl(v)) fail$1("Las imágenes deben guardarse en la Media Library, no dentro del template.");
1239
+ if (hasDataUrl(v)) fail$2("Las imágenes deben guardarse en la Media Library, no dentro del template.");
1142
1240
  return v;
1143
1241
  };
1144
1242
  const text = (v, fallback) => typeof v === "string" ? v : fallback;
@@ -1168,7 +1266,7 @@ const validateTemplatePatch = (input) => {
1168
1266
  if ("safeMargin" in t) out.safeMargin = nonNegative(t.safeMargin, "El margen seguro");
1169
1267
  if ("gridUnit" in t) out.gridUnit = nonNegative(t.gridUnit, "La cuadrícula");
1170
1268
  if ("layers" in t) out.layers = layers(t.layers);
1171
- if (!Object.keys(out).length) fail$1("Nada que actualizar.");
1269
+ if (!Object.keys(out).length) fail$2("Nada que actualizar.");
1172
1270
  return out;
1173
1271
  };
1174
1272
  const TEMPLATE_UID = "plugin::strapi-content-generator-gemini.template";
@@ -1284,7 +1382,7 @@ const templates$1 = ({ strapi }) => {
1284
1382
  { get: (p) => store().get(p), set: (p) => store().set(p) }
1285
1383
  );
1286
1384
  };
1287
- const fail = (ctx, status, name2, message) => {
1385
+ const fail$1 = (ctx, status, name2, message) => {
1288
1386
  ctx.status = status;
1289
1387
  ctx.body = { data: null, error: { status, name: name2, message } };
1290
1388
  };
@@ -1295,10 +1393,10 @@ const templates = ({ strapi }) => {
1295
1393
  ctx.body = await work();
1296
1394
  ctx.status = status;
1297
1395
  } catch (err) {
1298
- if (err instanceof TemplateInputError) return fail(ctx, 400, "BadRequestError", err.message);
1299
- if (err instanceof TemplateNotFoundError) return fail(ctx, 404, "NotFoundError", err.message);
1396
+ if (err instanceof TemplateInputError) return fail$1(ctx, 400, "BadRequestError", err.message);
1397
+ if (err instanceof TemplateNotFoundError) return fail$1(ctx, 404, "NotFoundError", err.message);
1300
1398
  strapi.log.error(`[templates] ${err instanceof Error ? err.message : String(err)}`);
1301
- return fail(ctx, 500, "InternalServerError", "No se pudo completar la operación con los templates.");
1399
+ return fail$1(ctx, 500, "InternalServerError", "No se pudo completar la operación con los templates.");
1302
1400
  }
1303
1401
  };
1304
1402
  const body = (ctx) => ctx.request.body ?? {};
@@ -1322,11 +1420,1261 @@ const templates = ({ strapi }) => {
1322
1420
  deleteFolder: (ctx) => run(ctx, async () => ({ folders: await svc().deleteFolder(body(ctx).path) }))
1323
1421
  };
1324
1422
  };
1423
+ const DRIVE_SCOPE = "https://www.googleapis.com/auth/drive";
1424
+ const OAUTH_STATE_KEY = "DRIVE_OAUTH_STATE";
1425
+ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
1426
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
1427
+ const REVOKE_URL = "https://oauth2.googleapis.com/revoke";
1428
+ const STATE_TTL_MS = 10 * 6e4;
1429
+ class DriveAuthError extends Error {
1430
+ constructor(message) {
1431
+ super(message);
1432
+ this.name = "DriveAuthError";
1433
+ }
1434
+ }
1435
+ const authUrl = (client, redirectUri, state) => {
1436
+ const params = new URLSearchParams({
1437
+ client_id: client.clientId,
1438
+ redirect_uri: redirectUri,
1439
+ response_type: "code",
1440
+ scope: DRIVE_SCOPE,
1441
+ // Sin `offline` no hay refresh token; sin `consent`, Google no lo repite si la cuenta ya autorizó antes.
1442
+ access_type: "offline",
1443
+ prompt: "consent",
1444
+ include_granted_scopes: "true",
1445
+ state
1446
+ });
1447
+ return `${AUTH_URL}?${params}`;
1448
+ };
1449
+ const newState = async (store, now2 = Date.now()) => {
1450
+ const value = node_crypto.randomBytes(24).toString("hex");
1451
+ await store.set({ key: OAUTH_STATE_KEY, value: { value, expiresAt: now2 + STATE_TTL_MS } });
1452
+ return value;
1453
+ };
1454
+ const checkState = async (store, state, now2 = Date.now()) => {
1455
+ const saved = await store.get({ key: OAUTH_STATE_KEY });
1456
+ await store.delete({ key: OAUTH_STATE_KEY });
1457
+ return typeof state === "string" && !!saved?.value && saved.value === state && (saved.expiresAt ?? 0) >= now2;
1458
+ };
1459
+ const postToken = async (fetchFn, body) => {
1460
+ const res = await fetchFn(TOKEN_URL, {
1461
+ method: "POST",
1462
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1463
+ body: new URLSearchParams(body).toString()
1464
+ });
1465
+ const data = await res.json().catch(() => ({}));
1466
+ if (!res.ok || !data.access_token) throw new DriveAuthError(`Google rechazó la conexión (${data.error ?? `HTTP ${res.status}`}).`);
1467
+ return data;
1468
+ };
1469
+ const exchangeCode = async ({
1470
+ client,
1471
+ code,
1472
+ redirectUri,
1473
+ fetch: fetchFn
1474
+ }) => {
1475
+ const data = await postToken(fetchFn, {
1476
+ code,
1477
+ client_id: client.clientId,
1478
+ client_secret: client.clientSecret,
1479
+ redirect_uri: redirectUri,
1480
+ grant_type: "authorization_code"
1481
+ });
1482
+ if (!data.refresh_token) throw new DriveAuthError("Google no entregó un token de actualización. Intenta conectar de nuevo.");
1483
+ return { accessToken: data.access_token, expiresIn: data.expires_in ?? 3600, refreshToken: data.refresh_token };
1484
+ };
1485
+ const refreshAccessToken = async ({
1486
+ client,
1487
+ refreshToken,
1488
+ fetch: fetchFn
1489
+ }) => {
1490
+ const data = await postToken(fetchFn, {
1491
+ client_id: client.clientId,
1492
+ client_secret: client.clientSecret,
1493
+ refresh_token: refreshToken,
1494
+ grant_type: "refresh_token"
1495
+ });
1496
+ return { accessToken: data.access_token, expiresIn: data.expires_in ?? 3600 };
1497
+ };
1498
+ const revokeToken = async (token, fetchFn) => {
1499
+ await fetchFn(REVOKE_URL, {
1500
+ method: "POST",
1501
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1502
+ body: new URLSearchParams({ token })
1503
+ });
1504
+ };
1505
+ const API = "https://www.googleapis.com/drive/v3";
1506
+ const MAX_RETRIES = 4;
1507
+ const EXPIRY_MARGIN_MS = 6e4;
1508
+ const RATE_REASONS = /* @__PURE__ */ new Set(["rateLimitExceeded", "userRateLimitExceeded"]);
1509
+ class DriveNotConnectedError extends Error {
1510
+ constructor() {
1511
+ super("Google Drive no está conectado.");
1512
+ this.name = "DriveNotConnectedError";
1513
+ }
1514
+ }
1515
+ class DriveApiError extends Error {
1516
+ constructor(status, message) {
1517
+ super(message);
1518
+ this.status = status;
1519
+ this.name = "DriveApiError";
1520
+ }
1521
+ }
1522
+ const isRetryable = (status, body) => status === 429 || status >= 500 || status === 403 && !!body.error?.errors?.some((e) => RATE_REASONS.has(e.reason ?? ""));
1523
+ const createDriveClient = ({
1524
+ getCredentials,
1525
+ fetch: fetchFn,
1526
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
1527
+ now: now2 = Date.now
1528
+ }) => {
1529
+ let cached = null;
1530
+ const accessToken = async (force = false) => {
1531
+ if (!force && cached && cached.expiresAt - EXPIRY_MARGIN_MS > now2()) return cached.token;
1532
+ const credentials = await getCredentials();
1533
+ if (!credentials) throw new DriveNotConnectedError();
1534
+ const { accessToken: token, expiresIn } = await refreshAccessToken({ ...credentials, fetch: fetchFn });
1535
+ cached = { token, expiresAt: now2() + expiresIn * 1e3 };
1536
+ return token;
1537
+ };
1538
+ const send = async (path, init = {}) => {
1539
+ const url = path.startsWith("https://") ? path : `${API}${path}`;
1540
+ let renewed = false;
1541
+ for (let attempt = 0; ; attempt++) {
1542
+ const headers = new Headers(init.headers);
1543
+ headers.set("authorization", `Bearer ${await accessToken()}`);
1544
+ const res = await fetchFn(url, { ...init, headers });
1545
+ if (res.ok) return res;
1546
+ const body = await res.json().catch(() => ({}));
1547
+ if (res.status === 401 && !renewed) {
1548
+ renewed = true;
1549
+ await accessToken(true);
1550
+ continue;
1551
+ }
1552
+ if (attempt < MAX_RETRIES && isRetryable(res.status, body)) {
1553
+ await sleep(1e3 * 2 ** attempt);
1554
+ continue;
1555
+ }
1556
+ throw new DriveApiError(res.status, `Drive respondió ${res.status}: ${body.error?.message ?? res.statusText}`);
1557
+ }
1558
+ };
1559
+ const request = async (path, init = {}) => {
1560
+ const res = await send(path, init);
1561
+ return res.status === 204 ? null : await res.json();
1562
+ };
1563
+ const download2 = async (path) => new Uint8Array(await (await send(path)).arrayBuffer());
1564
+ const about = async () => {
1565
+ const params = new URLSearchParams({ fields: "user(emailAddress),storageQuota(limit,usage)" });
1566
+ const data = await request(`/about?${params}`);
1567
+ return {
1568
+ email: data.user?.emailAddress ?? "",
1569
+ usage: Number(data.storageQuota?.usage ?? 0),
1570
+ limit: data.storageQuota?.limit ? Number(data.storageQuota.limit) : null
1571
+ };
1572
+ };
1573
+ return { request, download: download2, about, reset: () => void (cached = null) };
1574
+ };
1575
+ const DRIVE_CLIENT_KEY = "DRIVE_CLIENT";
1576
+ const asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1577
+ const readJson = (value) => {
1578
+ if (typeof value !== "string") return null;
1579
+ try {
1580
+ return asObject(JSON.parse(value));
1581
+ } catch {
1582
+ return null;
1583
+ }
1584
+ };
1585
+ const isText = (v) => typeof v === "string" && v.trim() !== "";
1586
+ const readClient = (data) => {
1587
+ const web = asObject(data.web);
1588
+ if (!web) return { ok: false, message: "El cliente debe ser de tipo Aplicación web." };
1589
+ const uris = Array.isArray(web.redirect_uris) ? web.redirect_uris.filter(isText) : [];
1590
+ const missing = [
1591
+ !isText(web.client_id) && "client_id",
1592
+ !isText(web.client_secret) && "client_secret",
1593
+ uris.length === 0 && "redirect_uris"
1594
+ ].filter(Boolean);
1595
+ if (missing.length) return { ok: false, message: `Al JSON del cliente le falta ${missing.join(", ")}.` };
1596
+ return {
1597
+ ok: true,
1598
+ client: { clientId: web.client_id, clientSecret: web.client_secret, redirectUris: uris }
1599
+ };
1600
+ };
1601
+ const parseClientConfig = (value, redirectUri) => {
1602
+ const data = readJson(value);
1603
+ if (!data) return { ok: false, message: "El archivo no es un JSON válido." };
1604
+ const result = readClient(data);
1605
+ if ("client" in result && !result.client.redirectUris.includes(redirectUri)) {
1606
+ return { ok: false, message: `Este cliente no tiene autorizada la URL ${redirectUri}. Agrégala en Google Cloud.` };
1607
+ }
1608
+ return result;
1609
+ };
1610
+ const storedClient = (client) => JSON.stringify({ web: { client_id: client.clientId, client_secret: client.clientSecret, redirect_uris: client.redirectUris } });
1611
+ const loadClient = (stored) => {
1612
+ const data = readJson(stored);
1613
+ if (!data) return null;
1614
+ const result = readClient(data);
1615
+ return "client" in result ? result.client : null;
1616
+ };
1617
+ const publicClient = (stored) => {
1618
+ const client = loadClient(stored);
1619
+ if (!client) return { configured: false, clientId: null, last4: null };
1620
+ return { configured: true, clientId: client.clientId, last4: maskSecret(client.clientSecret).last4 };
1621
+ };
1622
+ const DRIVE_ROOT = "root";
1623
+ const FOLDER_MIME = "application/vnd.google-apps.folder";
1624
+ const UPLOAD = "https://www.googleapis.com/upload/drive/v3/files";
1625
+ const quote = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1626
+ const findChild = async (http, parentId, name2) => {
1627
+ const params = new URLSearchParams({
1628
+ q: `${quote(parentId)} in parents and name = ${quote(name2)} and trashed = false`,
1629
+ fields: "files(id,name,mimeType)",
1630
+ pageSize: "10",
1631
+ spaces: "drive"
1632
+ });
1633
+ const { files } = await http.request(`/files?${params}`);
1634
+ return files?.[0] ?? null;
1635
+ };
1636
+ const createFolder = async (http, parentId, name2) => {
1637
+ const { id } = await http.request("/files?fields=id", {
1638
+ method: "POST",
1639
+ headers: { "content-type": "application/json" },
1640
+ body: JSON.stringify({ name: name2, mimeType: FOLDER_MIME, parents: [parentId] })
1641
+ });
1642
+ return id;
1643
+ };
1644
+ const multipart = (metadata, mime, content) => {
1645
+ const boundary = `iw-${node_crypto.randomBytes(12).toString("hex")}`;
1646
+ const head = `--${boundary}\r
1647
+ Content-Type: application/json; charset=UTF-8\r
1648
+ \r
1649
+ ${JSON.stringify(metadata)}\r
1650
+ --${boundary}\r
1651
+ Content-Type: ${mime}\r
1652
+ \r
1653
+ `;
1654
+ const body = Buffer.concat([Buffer.from(head, "utf8"), Buffer.from(content), Buffer.from(`\r
1655
+ --${boundary}--`, "utf8")]);
1656
+ return { body, contentType: `multipart/related; boundary=${boundary}` };
1657
+ };
1658
+ const uploadFile = async (http, parentId, file) => {
1659
+ const metadata = { name: file.name, parents: [parentId], ...file.description ? { description: file.description } : {} };
1660
+ const { body, contentType } = multipart(metadata, file.mime, file.content);
1661
+ const { id, md5Checksum } = await http.request(`${UPLOAD}?uploadType=multipart&fields=id,md5Checksum`, {
1662
+ method: "POST",
1663
+ headers: { "content-type": contentType },
1664
+ body
1665
+ });
1666
+ return { id, md5: md5Checksum ?? null };
1667
+ };
1668
+ const updateContent = async (http, driveId, file) => {
1669
+ const metadata = { name: file.name, ...file.description ? { description: file.description } : {} };
1670
+ const { body, contentType } = multipart(metadata, file.mime, file.content);
1671
+ const { md5Checksum } = await http.request(
1672
+ `${UPLOAD}/${encodeURIComponent(driveId)}?uploadType=multipart&fields=id,md5Checksum`,
1673
+ { method: "PATCH", headers: { "content-type": contentType }, body }
1674
+ );
1675
+ return { id: driveId, md5: md5Checksum ?? null };
1676
+ };
1677
+ const updateMeta = async (http, driveId, { name: name2, from, to }) => {
1678
+ const params = new URLSearchParams({ fields: "id" });
1679
+ if (to && from && to !== from) {
1680
+ params.set("addParents", to);
1681
+ params.set("removeParents", from);
1682
+ }
1683
+ await http.request(`/files/${encodeURIComponent(driveId)}?${params}`, {
1684
+ method: "PATCH",
1685
+ headers: { "content-type": "application/json" },
1686
+ body: JSON.stringify(name2 ? { name: name2 } : {})
1687
+ });
1688
+ };
1689
+ const trash = async (http, driveId) => {
1690
+ try {
1691
+ await http.request(`/files/${encodeURIComponent(driveId)}?fields=id`, {
1692
+ method: "PATCH",
1693
+ headers: { "content-type": "application/json" },
1694
+ body: JSON.stringify({ trashed: true })
1695
+ });
1696
+ } catch (err) {
1697
+ if (err instanceof DriveApiError && err.status === 404) return;
1698
+ throw err;
1699
+ }
1700
+ };
1701
+ const download = async (http, driveId) => {
1702
+ if (!http.download) throw new Error("El cliente de Drive no puede descargar.");
1703
+ return http.download(`/files/${encodeURIComponent(driveId)}?alt=media`);
1704
+ };
1705
+ const ENTRY_FIELDS = "id,name,mimeType,size,modifiedTime,webViewLink,parents";
1706
+ const toEntry = (f) => ({
1707
+ id: f.id,
1708
+ name: f.name,
1709
+ mimeType: f.mimeType,
1710
+ size: f.size ? Number(f.size) : null,
1711
+ modifiedTime: f.modifiedTime ?? null,
1712
+ webViewLink: f.webViewLink ?? null,
1713
+ parents: f.parents ?? []
1714
+ });
1715
+ const listChildren = async (http, parentId, pageToken) => {
1716
+ const params = new URLSearchParams({
1717
+ q: `${quote(parentId)} in parents and trashed = false`,
1718
+ fields: `nextPageToken,files(${ENTRY_FIELDS})`,
1719
+ orderBy: "folder,name_natural",
1720
+ pageSize: "200",
1721
+ spaces: "drive"
1722
+ });
1723
+ if (pageToken) params.set("pageToken", pageToken);
1724
+ const data = await http.request(`/files?${params}`);
1725
+ return { items: (data.files ?? []).map(toEntry), nextPageToken: data.nextPageToken ?? null };
1726
+ };
1727
+ const getFile = async (http, id) => toEntry(await http.request(`/files/${encodeURIComponent(id)}?fields=${ENTRY_FIELDS}`));
1728
+ const countTree = async (http, folderId, limit = 1e3) => {
1729
+ let files = 0;
1730
+ let folders = 0;
1731
+ const queue = [folderId];
1732
+ while (queue.length && files + folders < limit) {
1733
+ const parent = queue.shift();
1734
+ let token;
1735
+ do {
1736
+ const page = await listChildren(http, parent, token);
1737
+ for (const item of page.items) {
1738
+ if (item.mimeType === FOLDER_MIME) {
1739
+ folders++;
1740
+ queue.push(item.id);
1741
+ } else files++;
1742
+ }
1743
+ token = page.nextPageToken ?? void 0;
1744
+ } while (token && files + folders < limit);
1745
+ }
1746
+ return { files, folders, truncated: files + folders >= limit };
1747
+ };
1748
+ const getMeta = async (http, id) => {
1749
+ try {
1750
+ return await http.request(
1751
+ `/files/${encodeURIComponent(id)}?fields=id,name,mimeType,parents,trashed,md5Checksum,size`
1752
+ );
1753
+ } catch (err) {
1754
+ if (err instanceof DriveApiError && err.status === 404) return null;
1755
+ throw err;
1756
+ }
1757
+ };
1758
+ const rootId = async (http) => (await http.request("/files/root?fields=id")).id;
1759
+ const startPageToken = async (http) => (await http.request("/changes/startPageToken")).startPageToken;
1760
+ const CHANGE_FIELDS = "nextPageToken,newStartPageToken,changes(fileId,removed,file(id,name,mimeType,parents,trashed,md5Checksum,size))";
1761
+ const listChanges = async (http, pageToken) => {
1762
+ const params = new URLSearchParams({ pageToken, fields: CHANGE_FIELDS, includeRemoved: "true", spaces: "drive", pageSize: "1000" });
1763
+ return http.request(`/changes?${params}`);
1764
+ };
1765
+ const driveOps = (http) => ({
1766
+ findChild: (parentId, name2) => findChild(http, parentId, name2),
1767
+ createFolder: (parentId, name2) => createFolder(http, parentId, name2),
1768
+ uploadFile: (parentId, file) => uploadFile(http, parentId, file),
1769
+ updateContent: (driveId, file) => updateContent(http, driveId, file),
1770
+ updateMeta: (driveId, change) => updateMeta(http, driveId, change),
1771
+ trash: (driveId) => trash(http, driveId),
1772
+ download: (driveId) => download(http, driveId),
1773
+ startPageToken: () => startPageToken(http),
1774
+ rootId: () => rootId(http),
1775
+ listChildren: (parentId, pageToken) => listChildren(http, parentId, pageToken),
1776
+ getFile: (id) => getFile(http, id),
1777
+ getMeta: (id) => getMeta(http, id),
1778
+ countTree: (folderId) => countTree(http, folderId),
1779
+ listChanges: (pageToken) => listChanges(http, pageToken)
1780
+ });
1781
+ class DriveSyncError extends Error {
1782
+ constructor(message, status = 400) {
1783
+ super(message);
1784
+ this.status = status;
1785
+ this.name = "DriveSyncError";
1786
+ }
1787
+ }
1788
+ const collisionMessage = (name2) => `Ya existe «${name2}» en Drive y no se sincronizó desde el CMS. Cambia el nombre para sincronizarla.`;
1789
+ const pathIds = (path) => path.split("/").filter(Boolean).map(Number).filter((n) => Number.isInteger(n));
1790
+ const OUTSIDE = "Solo se sincroniza lo que está dentro de «Generated».";
1791
+ const ROOT_ITSELF = "Marca una carpeta o archivo dentro de «Generated», no la raíz.";
1792
+ const load = async (cms, cmsType, cmsId) => {
1793
+ if (cmsType === "folder") {
1794
+ const folder = await cms.folder(cmsId);
1795
+ if (folder) return { type: "folder", folder };
1796
+ } else {
1797
+ const file = await cms.file(cmsId);
1798
+ if (file) return { type: "file", file };
1799
+ }
1800
+ throw new DriveSyncError("El elemento ya no existe en la Biblioteca.");
1801
+ };
1802
+ const ancestorsOf = async (cms, root, path) => {
1803
+ const ids = pathIds(path);
1804
+ if (ids[0] !== root.pathId) return null;
1805
+ const below = ids.slice(1);
1806
+ if (!below.length) return [];
1807
+ const folders = await cms.foldersByPathIds(below);
1808
+ const byPathId = new Map(folders.map((f) => [f.pathId, f]));
1809
+ return below.map((p) => byPathId.get(p)).filter((f) => !!f);
1810
+ };
1811
+ const containerPath = (item) => item.type === "folder" ? item.folder.path.slice(0, item.folder.path.lastIndexOf("/")) || "/" : item.file.folderPath;
1812
+ const ensureFolderLink = async (deps, folder, parentDriveId, root) => {
1813
+ const existing = await deps.links.find("folder", folder.id);
1814
+ if (existing?.driveId) return root && !existing.root ? deps.links.update(existing.id, { root: true }) : existing;
1815
+ if (await deps.drive.findChild(parentDriveId, folder.name)) throw new DriveSyncError(collisionMessage(folder.name), 409);
1816
+ const driveId = await deps.drive.createFolder(parentDriveId, folder.name);
1817
+ const data = { driveId, name: folder.name, parentCmsId: folder.parentId, status: "synced", error: null };
1818
+ return existing ? deps.links.update(existing.id, { ...data, root: root || existing.root }) : deps.links.create({ cmsType: "folder", cmsId: folder.id, root, version: null, driveRev: null, ...data });
1819
+ };
1820
+ const mark = async (deps, cmsType, cmsId) => {
1821
+ const root = await deps.cms.generatedRoot();
1822
+ if (!root) throw new DriveSyncError(OUTSIDE);
1823
+ if (cmsType === "folder" && cmsId === root.id) throw new DriveSyncError(ROOT_ITSELF);
1824
+ const item = await load(deps.cms, cmsType, cmsId);
1825
+ const ancestors = await ancestorsOf(deps.cms, root, containerPath(item));
1826
+ if (!ancestors || item.type === "file" && item.file.folderId === null) throw new DriveSyncError(OUTSIDE);
1827
+ let parentDriveId = DRIVE_ROOT;
1828
+ for (const folder of ancestors) parentDriveId = (await ensureFolderLink(deps, folder, parentDriveId, false)).driveId;
1829
+ if (item.type === "folder") {
1830
+ await ensureFolderLink(deps, item.folder, parentDriveId, true);
1831
+ } else {
1832
+ const existing = await deps.links.find("file", item.file.id);
1833
+ if (existing) {
1834
+ if (!existing.root) await deps.links.update(existing.id, { root: true });
1835
+ } else {
1836
+ if (await deps.drive.findChild(parentDriveId, item.file.name)) throw new DriveSyncError(collisionMessage(item.file.name), 409);
1837
+ await deps.links.create({
1838
+ cmsType: "file",
1839
+ cmsId: item.file.id,
1840
+ driveId: null,
1841
+ root: true,
1842
+ name: null,
1843
+ parentCmsId: null,
1844
+ version: null,
1845
+ driveRev: null,
1846
+ status: "pending",
1847
+ error: null
1848
+ });
1849
+ }
1850
+ }
1851
+ await deps.ops.push(cmsType, cmsId);
1852
+ };
1853
+ const neededLinkIds = async (deps) => {
1854
+ const all3 = await deps.links.all();
1855
+ const rootPaths = [];
1856
+ const ancestorPathIds = /* @__PURE__ */ new Set();
1857
+ const keep = /* @__PURE__ */ new Set();
1858
+ for (const link of all3.filter((l) => l.root)) {
1859
+ keep.add(link.id);
1860
+ if (link.cmsType === "folder") {
1861
+ const folder = await deps.cms.folder(link.cmsId);
1862
+ if (!folder) continue;
1863
+ rootPaths.push(folder.path);
1864
+ pathIds(folder.path).slice(0, -1).forEach((p) => ancestorPathIds.add(p));
1865
+ } else {
1866
+ const file = await deps.cms.file(link.cmsId);
1867
+ if (file) pathIds(file.folderPath).forEach((p) => ancestorPathIds.add(p));
1868
+ }
1869
+ }
1870
+ const under = (path) => rootPaths.some((r) => path === r || path.startsWith(`${r}/`));
1871
+ for (const link of all3) {
1872
+ if (keep.has(link.id)) continue;
1873
+ if (link.cmsType === "folder") {
1874
+ const folder = await deps.cms.folder(link.cmsId);
1875
+ if (folder && (under(folder.path) || ancestorPathIds.has(folder.pathId))) keep.add(link.id);
1876
+ } else {
1877
+ const file = await deps.cms.file(link.cmsId);
1878
+ if (file && under(file.folderPath)) keep.add(link.id);
1879
+ }
1880
+ }
1881
+ return keep;
1882
+ };
1883
+ const unmark = async (deps, cmsType, cmsId) => {
1884
+ const link = await deps.links.find(cmsType, cmsId);
1885
+ if (!link?.root) throw new DriveSyncError("Ese elemento no está marcado para sincronizar.");
1886
+ await deps.links.update(link.id, { root: false });
1887
+ const keep = await neededLinkIds(deps);
1888
+ for (const l of await deps.links.all()) if (!keep.has(l.id)) await deps.links.remove(l.id);
1889
+ };
1890
+ const isSynced = async (deps, cmsType, cmsId) => {
1891
+ const all3 = await deps.links.all();
1892
+ const roots = all3.filter((l) => l.root);
1893
+ if (roots.some((l) => l.cmsType === cmsType && l.cmsId === cmsId)) return true;
1894
+ const path = cmsType === "folder" ? (await deps.cms.folder(cmsId))?.path : (await deps.cms.file(cmsId))?.folderPath;
1895
+ if (!path) return false;
1896
+ for (const l of roots.filter((r) => r.cmsType === "folder")) {
1897
+ const folder = await deps.cms.folder(l.cmsId);
1898
+ if (folder && (path === folder.path ? cmsType === "file" : path.startsWith(`${folder.path}/`))) return true;
1899
+ }
1900
+ return false;
1901
+ };
1902
+ const FOLDER_UID = "plugin::upload.folder";
1903
+ const FILE_UID = "plugin::upload.file";
1904
+ const LINK_UID = "plugin::strapi-content-generator-gemini.drive-link";
1905
+ const OP_UID = "plugin::strapi-content-generator-gemini.drive-op";
1906
+ const toFolder = (r) => ({
1907
+ id: r.id,
1908
+ pathId: Number(r.pathId),
1909
+ name: r.name,
1910
+ path: r.path,
1911
+ parentId: r.parent?.id ?? null
1912
+ });
1913
+ const toFile = (r) => ({
1914
+ id: r.id,
1915
+ name: r.name,
1916
+ folderId: r.folder?.id ?? null,
1917
+ folderPath: r.folderPath ?? "/",
1918
+ url: r.url ?? "",
1919
+ size: Number(r.size ?? 0),
1920
+ mime: r.mime ?? "application/octet-stream"
1921
+ });
1922
+ const strapiCms = (strapi, fetchFn = fetch) => {
1923
+ const folders = () => strapi.db.query(FOLDER_UID);
1924
+ const files = () => strapi.db.query(FILE_UID);
1925
+ const withParent = { populate: ["parent"] };
1926
+ const withFolder = { populate: ["folder"] };
1927
+ return {
1928
+ async generatedRoot() {
1929
+ const rows = await folders().findMany({ where: { name: "Generated" }, ...withParent });
1930
+ const root = rows.find((r) => !r.parent);
1931
+ return root ? toFolder(root) : null;
1932
+ },
1933
+ async folder(id) {
1934
+ const r = await folders().findOne({ where: { id }, ...withParent });
1935
+ return r ? toFolder(r) : null;
1936
+ },
1937
+ async file(id) {
1938
+ const r = await files().findOne({ where: { id }, ...withFolder });
1939
+ return r ? toFile(r) : null;
1940
+ },
1941
+ async foldersByPathIds(pathIds2) {
1942
+ if (!pathIds2.length) return [];
1943
+ return (await folders().findMany({ where: { pathId: { $in: pathIds2 } }, ...withParent })).map(toFolder);
1944
+ },
1945
+ async childFolders(folderId) {
1946
+ return (await folders().findMany({ where: { parent: { id: folderId } }, ...withParent })).map(toFolder);
1947
+ },
1948
+ async childFiles(folderId) {
1949
+ return (await files().findMany({ where: { folder: { id: folderId } }, ...withFolder })).map(toFile);
1950
+ },
1951
+ async readFile(file) {
1952
+ if (/^https?:\/\//.test(file.url)) {
1953
+ const res = await fetchFn(file.url);
1954
+ if (!res.ok) throw new Error(`No se pudo leer ${file.name} (${res.status}).`);
1955
+ return new Uint8Array(await res.arrayBuffer());
1956
+ }
1957
+ return new Uint8Array(await promises.readFile(node_path.join(strapi.dirs.static.public, file.url)));
1958
+ }
1959
+ };
1960
+ };
1961
+ const strapiCmsWriter = (strapi) => {
1962
+ const upload = () => strapi.plugin("upload").service("upload");
1963
+ const folderService = () => strapi.plugin("upload").service("folder");
1964
+ const fileById = async (id) => toFile(await strapi.db.query(FILE_UID).findOne({ where: { id }, populate: ["folder"] }));
1965
+ const system = { user: { id: null } };
1966
+ const withTempFile = async (name2, mime, content, work) => {
1967
+ const filepath = node_path.join(node_os.tmpdir(), `iw-drive-${node_crypto.randomBytes(8).toString("hex")}`);
1968
+ await promises.writeFile(filepath, content);
1969
+ try {
1970
+ return await work({ filepath, originalFilename: name2, mimetype: mime, size: content.length });
1971
+ } finally {
1972
+ await promises.rm(filepath, { force: true });
1973
+ }
1974
+ };
1975
+ return {
1976
+ uploadFile: (folderId, name2, mime, content) => withTempFile(name2, mime, content, async (file) => {
1977
+ const [row] = await upload().upload({ data: { fileInfo: { name: name2, folder: folderId } }, files: file });
1978
+ return fileById(row.id);
1979
+ }),
1980
+ replaceFile: async (id, name2, mime, content) => {
1981
+ const current = await strapi.db.query(FILE_UID).findOne({ where: { id }, populate: ["folder"] });
1982
+ return withTempFile(name2, mime, content, async (file) => {
1983
+ await upload().replace(id, { data: { fileInfo: { name: name2, folder: current?.folder?.id ?? null } }, file });
1984
+ return fileById(id);
1985
+ });
1986
+ },
1987
+ updateFile: async (id, { name: name2, folderId }) => {
1988
+ await upload().updateFileInfo(id, { name: name2, ...folderId !== void 0 ? { folder: folderId } : {} });
1989
+ },
1990
+ deleteFile: async (id) => {
1991
+ const row = await strapi.db.query(FILE_UID).findOne({ where: { id } });
1992
+ if (row) await upload().remove(row);
1993
+ },
1994
+ createFolder: async (name2, parentId) => {
1995
+ const row = await folderService().create({ name: name2, parent: parentId });
1996
+ return toFolder({ ...row, parent: { id: parentId } });
1997
+ },
1998
+ updateFolder: async (id, { name: name2, parentId }) => {
1999
+ const current = await strapi.db.query(FOLDER_UID).findOne({ where: { id }, populate: ["parent"] });
2000
+ if (!current) return;
2001
+ await folderService().update(id, { name: name2 ?? current.name, ...parentId !== void 0 ? { parent: parentId } : {} }, system);
2002
+ },
2003
+ deleteFolder: async (id) => {
2004
+ await folderService().deleteByIds([id]);
2005
+ }
2006
+ };
2007
+ };
2008
+ const toLink = (r) => ({
2009
+ id: r.id,
2010
+ cmsType: r.cmsType,
2011
+ cmsId: Number(r.cmsId),
2012
+ driveId: r.driveId ?? null,
2013
+ root: !!r.root,
2014
+ name: r.name ?? null,
2015
+ parentCmsId: r.parentCmsId ?? null,
2016
+ version: r.version ?? null,
2017
+ driveRev: r.driveRev ?? null,
2018
+ status: r.status ?? "pending",
2019
+ error: r.error ?? null
2020
+ });
2021
+ const strapiLinks = (strapi) => {
2022
+ const q = () => strapi.db.query(LINK_UID);
2023
+ return {
2024
+ all: async () => (await q().findMany({ orderBy: { id: "asc" } })).map(toLink),
2025
+ find: async (cmsType, cmsId) => {
2026
+ const r = await q().findOne({ where: { cmsType, cmsId } });
2027
+ return r ? toLink(r) : null;
2028
+ },
2029
+ findByDriveId: async (driveId) => {
2030
+ const r = await q().findOne({ where: { driveId } });
2031
+ return r ? toLink(r) : null;
2032
+ },
2033
+ create: async (data) => toLink(await q().create({ data })),
2034
+ update: async (id, data) => toLink(await q().update({ where: { id }, data })),
2035
+ remove: async (id) => void await q().delete({ where: { id } })
2036
+ };
2037
+ };
2038
+ const toOp = (r) => ({
2039
+ id: r.id,
2040
+ cmsType: r.cmsType,
2041
+ cmsId: Number(r.cmsId),
2042
+ attempts: Number(r.attempts ?? 0),
2043
+ runAfter: Number(r.runAfter ?? 0),
2044
+ error: r.error ?? null
2045
+ });
2046
+ const strapiOps = (strapi) => {
2047
+ const q = () => strapi.db.query(OP_UID);
2048
+ return {
2049
+ async push(cmsType, cmsId) {
2050
+ if (await q().findOne({ where: { cmsType, cmsId } })) return;
2051
+ await q().create({ data: { cmsType, cmsId, attempts: 0, runAfter: "0" } });
2052
+ },
2053
+ ready: async (limit, now2) => (await q().findMany({ where: { runAfter: { $lte: String(now2) } }, orderBy: { id: "asc" }, limit })).map(toOp),
2054
+ done: async (op) => void await q().delete({ where: { id: op.id } }),
2055
+ retry: async (op, error, runAfter) => void await q().update({ where: { id: op.id }, data: { attempts: op.attempts + 1, error, runAfter: String(runAfter) } }),
2056
+ count: async () => q().count()
2057
+ };
2058
+ };
2059
+ const MAX_FILE_BYTES = 30 * 1024 * 1024;
2060
+ const echo = new node_async_hooks.AsyncLocalStorage();
2061
+ const withoutEcho = (work) => echo.run(true, work);
2062
+ const isEcho = () => echo.getStore() === true;
2063
+ const versionOf = (file) => `${file.url}|${file.size}`;
2064
+ const PAGE_TOKEN_KEY = "DRIVE_PAGE_TOKEN";
2065
+ const ROOT_ID_KEY = "DRIVE_ROOT_ID";
2066
+ const FULL_CHECK_KEY = "DRIVE_FULL_CHECK";
2067
+ const NATIVE = "application/vnd.google-apps.";
2068
+ const MOVED_AWAY = "Se movió en Drive a una carpeta que no está sincronizada. Regrésalo en Drive o muévelo en el CMS.";
2069
+ const isNative = (f) => f.mimeType.startsWith(NATIVE) && f.mimeType !== FOLDER_MIME;
2070
+ const driveRootId = async (deps) => {
2071
+ const saved = await deps.store.get({ key: ROOT_ID_KEY });
2072
+ if (saved) return saved;
2073
+ const id = await deps.drive.rootId();
2074
+ await deps.store.set({ key: ROOT_ID_KEY, value: id });
2075
+ return id;
2076
+ };
2077
+ const cmsParentOf = async (deps, f, rootId2) => {
2078
+ const parent = f.parents?.[0];
2079
+ if (!parent) return null;
2080
+ if (parent === rootId2 || parent === DRIVE_ROOT) return (await deps.cms.generatedRoot())?.id ?? null;
2081
+ const link = await deps.links.findByDriveId(parent);
2082
+ return link?.cmsType === "folder" ? link.cmsId : null;
2083
+ };
2084
+ const pruneLinks = async (deps) => {
2085
+ const keep = await neededLinkIds(deps);
2086
+ for (const l of await deps.links.all()) if (!keep.has(l.id)) await deps.links.remove(l.id);
2087
+ };
2088
+ const dropDeadLinks = async (deps) => {
2089
+ for (const l of await deps.links.all()) {
2090
+ const exists = l.cmsType === "folder" ? await deps.cms.folder(l.cmsId) : await deps.cms.file(l.cmsId);
2091
+ if (!exists) await deps.links.remove(l.id);
2092
+ }
2093
+ };
2094
+ const removeInCms = async (deps, link) => {
2095
+ if (link.cmsType === "folder") await deps.writer.deleteFolder(link.cmsId);
2096
+ else await deps.writer.deleteFile(link.cmsId);
2097
+ deps.log?.info?.(`[drive] Drive → CMS: borrado «${link.name}»`);
2098
+ await deps.links.remove(link.id);
2099
+ if (link.cmsType === "folder") await dropDeadLinks(deps);
2100
+ };
2101
+ const updateLinked = async (deps, link, f, parentCms) => {
2102
+ let changed = false;
2103
+ if (link.cmsType === "folder") {
2104
+ if (f.name !== link.name || parentCms !== link.parentCmsId) {
2105
+ await deps.writer.updateFolder(link.cmsId, { name: f.name, parentId: parentCms });
2106
+ deps.log?.info?.(`[drive] Drive → CMS: carpeta «${link.name}» → «${f.name}»`);
2107
+ await deps.links.update(link.id, { name: f.name, parentCmsId: parentCms, status: "synced", error: null });
2108
+ return true;
2109
+ }
2110
+ return false;
2111
+ }
2112
+ if (f.md5Checksum && f.md5Checksum !== link.driveRev) {
2113
+ if (Number(f.size ?? 0) > MAX_FILE_BYTES) {
2114
+ await deps.links.update(link.id, { status: "skipped", error: "La versión de Drive pesa más de 30 MB; no se trae al CMS." });
2115
+ return true;
2116
+ }
2117
+ const content = await deps.drive.download(f.id);
2118
+ const cmsFile = await deps.writer.replaceFile(link.cmsId, f.name, f.mimeType, content);
2119
+ deps.log?.info?.(`[drive] Drive → CMS: contenido nuevo de «${f.name}»`);
2120
+ await deps.links.update(link.id, { version: versionOf(cmsFile), driveRev: f.md5Checksum, name: f.name, status: "synced", error: null });
2121
+ link = { ...link, name: f.name };
2122
+ changed = true;
2123
+ }
2124
+ if (f.name !== link.name || parentCms !== link.parentCmsId) {
2125
+ await deps.writer.updateFile(link.cmsId, { name: f.name, folderId: parentCms });
2126
+ deps.log?.info?.(`[drive] Drive → CMS: archivo «${link.name}» → «${f.name}»`);
2127
+ const cmsFile = await deps.cms.file(link.cmsId);
2128
+ await deps.links.update(link.id, { name: f.name, parentCmsId: parentCms, ...cmsFile ? { version: versionOf(cmsFile) } : {}, status: "synced", error: null });
2129
+ changed = true;
2130
+ }
2131
+ return changed;
2132
+ };
2133
+ const createInCms = async (deps, f, parentCms) => {
2134
+ if (f.mimeType === FOLDER_MIME) {
2135
+ const folder = await deps.writer.createFolder(f.name, parentCms);
2136
+ await deps.links.create({ cmsType: "folder", cmsId: folder.id, driveId: f.id, root: false, name: f.name, parentCmsId: parentCms, version: null, driveRev: null, status: "synced", error: null });
2137
+ return;
2138
+ }
2139
+ if (Number(f.size ?? 0) > MAX_FILE_BYTES) {
2140
+ deps.log?.warn(`[drive] «${f.name}» pesa más de 30 MB; no se trae al CMS.`);
2141
+ return;
2142
+ }
2143
+ const content = await deps.drive.download(f.id);
2144
+ const file = await deps.writer.uploadFile(parentCms, f.name, f.mimeType, content);
2145
+ await deps.links.create({
2146
+ cmsType: "file",
2147
+ cmsId: file.id,
2148
+ driveId: f.id,
2149
+ root: false,
2150
+ name: f.name,
2151
+ parentCmsId: parentCms,
2152
+ version: versionOf(file),
2153
+ driveRev: f.md5Checksum ?? null,
2154
+ status: "synced",
2155
+ error: null
2156
+ });
2157
+ };
2158
+ const applyChange = async (deps, change, rootId2) => {
2159
+ const link = await deps.links.findByDriveId(change.fileId);
2160
+ const f = change.file;
2161
+ if (change.removed || f?.trashed) {
2162
+ if (!link) return false;
2163
+ await removeInCms(deps, link);
2164
+ return true;
2165
+ }
2166
+ if (!f || isNative(f)) return false;
2167
+ const parentCms = await cmsParentOf(deps, f, rootId2);
2168
+ const parentInside = parentCms !== null && await isSynced(deps, "folder", parentCms);
2169
+ if (link) {
2170
+ const pathOnly = !link.root && !await isSynced(deps, link.cmsType, link.cmsId);
2171
+ if (parentCms === null) {
2172
+ if (f.name !== link.name) deps.log?.info?.(`[drive] «${f.name}» no se aplica: su carpeta en Drive (${f.parents?.[0] ?? "ninguna"}) no está vinculada`);
2173
+ if (pathOnly || link.status === "error" && link.error === MOVED_AWAY) return false;
2174
+ await deps.links.update(link.id, { status: "error", error: MOVED_AWAY });
2175
+ return true;
2176
+ }
2177
+ const changed = await updateLinked(deps, link, f, parentCms);
2178
+ if (!pathOnly && !link.root && !parentInside) {
2179
+ await deps.links.remove(link.id);
2180
+ await pruneLinks(deps);
2181
+ return true;
2182
+ }
2183
+ return changed;
2184
+ }
2185
+ if (!parentInside) return false;
2186
+ await createInCms(deps, f, parentCms);
2187
+ return true;
2188
+ };
2189
+ const fullCheck = async (deps, rootId2) => {
2190
+ let applied = 0;
2191
+ let errors = 0;
2192
+ const all3 = await deps.links.all();
2193
+ deps.log?.info?.(`[drive] revisión completa contra Drive: ${all3.length} vínculos`);
2194
+ for (const link of all3) {
2195
+ if (!link.driveId) continue;
2196
+ if (!await deps.links.find(link.cmsType, link.cmsId)) continue;
2197
+ try {
2198
+ const meta = await deps.drive.getMeta(link.driveId);
2199
+ if (await applyChange(deps, { fileId: link.driveId, removed: meta === null, ...meta ? { file: meta } : {} }, rootId2)) applied++;
2200
+ } catch (err) {
2201
+ errors++;
2202
+ deps.log?.warn(`[drive] revisión de ${link.cmsType} ${link.cmsId}: ${err.message}`);
2203
+ }
2204
+ }
2205
+ return { applied, errors };
2206
+ };
2207
+ const pullChanges = async (deps, { full = false } = {}) => {
2208
+ let token = await deps.store.get({ key: PAGE_TOKEN_KEY });
2209
+ const rootId2 = await driveRootId(deps);
2210
+ let applied = 0;
2211
+ let errors = 0;
2212
+ if (!token) {
2213
+ token = await deps.drive.startPageToken();
2214
+ await deps.store.set({ key: PAGE_TOKEN_KEY, value: token });
2215
+ await deps.store.delete({ key: FULL_CHECK_KEY });
2216
+ }
2217
+ if (full) await deps.store.delete({ key: FULL_CHECK_KEY });
2218
+ if (!await deps.store.get({ key: FULL_CHECK_KEY })) {
2219
+ const checked = await withoutEcho(() => fullCheck(deps, rootId2));
2220
+ await deps.store.set({ key: FULL_CHECK_KEY, value: true });
2221
+ applied += checked.applied;
2222
+ errors += checked.errors;
2223
+ }
2224
+ await withoutEcho(async () => {
2225
+ for (; ; ) {
2226
+ const page = await deps.drive.listChanges(token);
2227
+ for (const change of page.changes ?? []) {
2228
+ try {
2229
+ if (await applyChange(deps, change, rootId2)) applied++;
2230
+ } catch (err) {
2231
+ errors++;
2232
+ deps.log?.warn(`[drive] cambio de Drive ${change.fileId}: ${err.message}`);
2233
+ }
2234
+ }
2235
+ token = page.nextPageToken ?? page.newStartPageToken ?? token;
2236
+ await deps.store.set({ key: PAGE_TOKEN_KEY, value: token });
2237
+ if (!page.nextPageToken) break;
2238
+ }
2239
+ });
2240
+ return { applied, errors };
2241
+ };
2242
+ const LOCK_KEY = "DRIVE_LOCK";
2243
+ const LAST_RUN_KEY = "DRIVE_LAST_RUN";
2244
+ const LOCK_MS = 10 * 6e4;
2245
+ const BATCH = 50;
2246
+ const MAX_ATTEMPTS = 5;
2247
+ const TOO_BIG = "Pesa más de 30 MB; no se sube a Drive.";
2248
+ const bytesOf = (file) => file.size * 1e3;
2249
+ const now = (deps) => (deps.now ?? Date.now)();
2250
+ const driveParent = async (deps, parentCmsId, depth = 0) => {
2251
+ const root = await deps.cms.generatedRoot();
2252
+ if (parentCmsId === null || parentCmsId === root?.id) return DRIVE_ROOT;
2253
+ let link = await deps.links.find("folder", parentCmsId);
2254
+ if (!link?.driveId && depth < 20) {
2255
+ await processItem(deps, "folder", parentCmsId, depth + 1);
2256
+ link = await deps.links.find("folder", parentCmsId);
2257
+ }
2258
+ if (!link?.driveId) throw new Error(`La carpeta ${parentCmsId} todavía no está en Drive.`);
2259
+ return link.driveId;
2260
+ };
2261
+ const saveError = async (deps, link, cmsType, cmsId, error, status = "error") => {
2262
+ if (link) await deps.links.update(link.id, { status, error });
2263
+ else await deps.links.create({ cmsType, cmsId, driveId: null, root: false, name: null, parentCmsId: null, version: null, driveRev: null, status, error });
2264
+ };
2265
+ const syncFolder = async (deps, folder, link, depth) => {
2266
+ const parent = await driveParent(deps, folder.parentId, depth);
2267
+ if (!link?.driveId) {
2268
+ if (await deps.drive.findChild(parent, folder.name)) return saveError(deps, link, "folder", folder.id, collisionMessage(folder.name));
2269
+ const driveId = await deps.drive.createFolder(parent, folder.name);
2270
+ const data = { driveId, name: folder.name, parentCmsId: folder.parentId, status: "synced", error: null };
2271
+ if (link) await deps.links.update(link.id, data);
2272
+ else await deps.links.create({ cmsType: "folder", cmsId: folder.id, root: false, version: null, driveRev: null, ...data });
2273
+ } else if (link.name !== folder.name || link.parentCmsId !== folder.parentId) {
2274
+ const from = link.parentCmsId === folder.parentId ? parent : await driveParent(deps, link.parentCmsId, depth).catch(() => parent);
2275
+ await deps.drive.updateMeta(link.driveId, { name: folder.name, from, to: parent });
2276
+ await deps.links.update(link.id, { name: folder.name, parentCmsId: folder.parentId, status: "synced", error: null });
2277
+ }
2278
+ if (!link?.driveId) {
2279
+ for (const f of await deps.cms.childFolders(folder.id)) await deps.ops.push("folder", f.id);
2280
+ for (const f of await deps.cms.childFiles(folder.id)) await deps.ops.push("file", f.id);
2281
+ }
2282
+ };
2283
+ const syncFile = async (deps, file, link, depth) => {
2284
+ if (bytesOf(file) > MAX_FILE_BYTES) return saveError(deps, link, "file", file.id, TOO_BIG, "skipped");
2285
+ const parent = await driveParent(deps, file.folderId, depth);
2286
+ const version = versionOf(file);
2287
+ const upload = async () => ({ name: file.name, mime: file.mime, content: await deps.cms.readFile(file), description: await deps.describe?.(file) });
2288
+ if (!link?.driveId) {
2289
+ if (await deps.drive.findChild(parent, file.name)) return saveError(deps, link, "file", file.id, collisionMessage(file.name));
2290
+ const { id: driveId, md5 } = await deps.drive.uploadFile(parent, await upload());
2291
+ const data = { driveId, name: file.name, parentCmsId: file.folderId, version, driveRev: md5, status: "synced", error: null };
2292
+ if (link) await deps.links.update(link.id, data);
2293
+ else await deps.links.create({ cmsType: "file", cmsId: file.id, root: false, ...data });
2294
+ return;
2295
+ }
2296
+ let driveRev = link.driveRev;
2297
+ if (link.version !== version) driveRev = (await deps.drive.updateContent(link.driveId, await upload())).md5;
2298
+ if (link.name !== file.name || link.parentCmsId !== file.folderId) {
2299
+ const from = link.parentCmsId === file.folderId ? parent : await driveParent(deps, link.parentCmsId, depth).catch(() => parent);
2300
+ await deps.drive.updateMeta(link.driveId, { name: file.name, from, to: parent });
2301
+ }
2302
+ await deps.links.update(link.id, { name: file.name, parentCmsId: file.folderId, version, driveRev, status: "synced", error: null });
2303
+ };
2304
+ const processItem = async (deps, cmsType, cmsId, depth = 0) => {
2305
+ const link = await deps.links.find(cmsType, cmsId);
2306
+ const item = cmsType === "folder" ? await deps.cms.folder(cmsId) : await deps.cms.file(cmsId);
2307
+ if (!item) {
2308
+ if (link?.driveId) await deps.drive.trash(link.driveId);
2309
+ if (link) await deps.links.remove(link.id);
2310
+ return;
2311
+ }
2312
+ if (!await isSynced(deps, cmsType, cmsId)) {
2313
+ if (link && !(await neededLinkIds(deps)).has(link.id)) await deps.links.remove(link.id);
2314
+ return;
2315
+ }
2316
+ if (cmsType === "folder") await syncFolder(deps, item, link, depth);
2317
+ else await syncFile(deps, item, link, depth);
2318
+ };
2319
+ const reconcile = async (deps) => {
2320
+ const links = await deps.links.all();
2321
+ const linked = new Set(links.map((l) => `${l.cmsType}:${l.cmsId}`));
2322
+ for (const link of links) {
2323
+ if (link.cmsType === "file") {
2324
+ const file = await deps.cms.file(link.cmsId);
2325
+ if (!file || !link.driveId || file.name !== link.name || file.folderId !== link.parentCmsId || versionOf(file) !== link.version) {
2326
+ if (link.status !== "skipped" || !file || bytesOf(file) <= MAX_FILE_BYTES) await deps.ops.push("file", link.cmsId);
2327
+ }
2328
+ continue;
2329
+ }
2330
+ const folder = await deps.cms.folder(link.cmsId);
2331
+ if (!folder || !link.driveId || folder.name !== link.name || folder.parentId !== link.parentCmsId) {
2332
+ await deps.ops.push("folder", link.cmsId);
2333
+ if (!folder) continue;
2334
+ }
2335
+ if (!await isSynced(deps, "folder", link.cmsId)) continue;
2336
+ for (const f of await deps.cms.childFolders(folder.id)) if (!linked.has(`folder:${f.id}`)) await deps.ops.push("folder", f.id);
2337
+ for (const f of await deps.cms.childFiles(folder.id)) if (!linked.has(`file:${f.id}`)) await deps.ops.push("file", f.id);
2338
+ }
2339
+ };
2340
+ const acquire = async (deps) => {
2341
+ const lock = await deps.store.get({ key: LOCK_KEY });
2342
+ if (lock?.until && lock.until > now(deps)) return false;
2343
+ await deps.store.set({ key: LOCK_KEY, value: { until: now(deps) + LOCK_MS } });
2344
+ return true;
2345
+ };
2346
+ const runSync = async (deps, { budgetMs = 4 * 6e4, full = false } = {}) => {
2347
+ if (!await acquire(deps)) return { skipped: true, pulled: 0, processed: 0, errors: 0, pending: await deps.ops.count() };
2348
+ const started = now(deps);
2349
+ let processed = 0;
2350
+ let errors = 0;
2351
+ let pulled = 0;
2352
+ try {
2353
+ if (deps.writer) {
2354
+ const result = await pullChanges({ ...deps, writer: deps.writer }, { full });
2355
+ pulled = result.applied;
2356
+ errors += result.errors;
2357
+ }
2358
+ await reconcile(deps);
2359
+ while (now(deps) - started < budgetMs) {
2360
+ const batch = await deps.ops.ready(BATCH, now(deps));
2361
+ if (!batch.length) break;
2362
+ for (const op of batch) {
2363
+ try {
2364
+ await processItem(deps, op.cmsType, op.cmsId);
2365
+ await deps.ops.done(op);
2366
+ processed++;
2367
+ } catch (err) {
2368
+ const message = err instanceof Error ? err.message : String(err);
2369
+ errors++;
2370
+ if (err instanceof DriveSyncError || op.attempts + 1 >= MAX_ATTEMPTS) {
2371
+ await deps.ops.done(op);
2372
+ await saveError(deps, await deps.links.find(op.cmsType, op.cmsId), op.cmsType, op.cmsId, message);
2373
+ } else {
2374
+ await deps.ops.retry(op, message, now(deps) + 3e4 * 2 ** op.attempts);
2375
+ }
2376
+ deps.log?.warn(`[drive] ${op.cmsType} ${op.cmsId}: ${message}`);
2377
+ }
2378
+ }
2379
+ }
2380
+ } finally {
2381
+ await deps.store.set({ key: LAST_RUN_KEY, value: { at: now(deps), pulled, processed, errors } });
2382
+ deps.log?.info?.(`[drive] ciclo terminado: ${pulled} cambios de Drive traídos, ${processed} elementos subidos o revisados, ${errors} errores`);
2383
+ await deps.store.delete({ key: LOCK_KEY });
2384
+ }
2385
+ return { skipped: false, pulled, processed, errors, pending: await deps.ops.count() };
2386
+ };
2387
+ const debounce = (run, delayMs = 5e3, onError = () => {
2388
+ }) => {
2389
+ let timer = null;
2390
+ return () => {
2391
+ if (timer) clearTimeout(timer);
2392
+ timer = setTimeout(() => {
2393
+ timer = null;
2394
+ run().catch(onError);
2395
+ }, delayMs);
2396
+ timer.unref?.();
2397
+ };
2398
+ };
2399
+ const DRIVE_TOKEN_KEY = "DRIVE_TOKEN";
2400
+ const DRIVE_ORIGINS_KEY = "DRIVE_ORIGINS";
2401
+ class DriveInputError extends Error {
2402
+ constructor(message, status = 400) {
2403
+ super(message);
2404
+ this.status = status;
2405
+ }
2406
+ }
2407
+ const NOT_CONNECTED = { connected: false, email: null, usage: null, limit: null, error: null };
2408
+ const REVOKED = "Desconectada: vuelve a conectar la cuenta.";
2409
+ const createDriveService = ({
2410
+ store,
2411
+ fetch: fetchFn,
2412
+ log,
2413
+ sync
2414
+ }) => {
2415
+ const client = async () => loadClient(await store.get({ key: DRIVE_CLIENT_KEY }));
2416
+ const token = async () => await store.get({ key: DRIVE_TOKEN_KEY });
2417
+ const http = createDriveClient({
2418
+ fetch: fetchFn,
2419
+ getCredentials: async () => {
2420
+ const [c, t] = await Promise.all([client(), token()]);
2421
+ return c && t?.refreshToken ? { client: c, refreshToken: t.refreshToken } : null;
2422
+ }
2423
+ });
2424
+ const clientFor = async (redirectUri) => {
2425
+ const c = await client();
2426
+ if (!c) throw new DriveInputError("Primero carga el JSON del cliente OAuth.");
2427
+ if (typeof redirectUri !== "string" || !c.redirectUris.includes(redirectUri)) {
2428
+ throw new DriveInputError(`Este cliente no tiene autorizada la URL ${String(redirectUri)}. Agrégala en Google Cloud.`);
2429
+ }
2430
+ return c;
2431
+ };
2432
+ const forgetToken = async () => {
2433
+ const t = await token();
2434
+ if (t?.refreshToken) {
2435
+ await revokeToken(t.refreshToken, fetchFn).catch((err) => log.warn(`[drive] no se pudo revocar el token: ${err.message}`));
2436
+ }
2437
+ await store.delete({ key: DRIVE_TOKEN_KEY });
2438
+ http.reset();
2439
+ };
2440
+ const connected = async () => !!await client() && !!(await token())?.refreshToken;
2441
+ const describe = async (file) => {
2442
+ const origins = await store.get({ key: DRIVE_ORIGINS_KEY }) ?? {};
2443
+ const origin2 = origins[String(file.id)];
2444
+ if (!origin2) return void 0;
2445
+ return `Template: ${origin2.name}
2446
+ ${sync?.serverUrl ?? ""}/admin/plugins/strapi-content-generator-gemini-studio?tab=templates`;
2447
+ };
2448
+ const syncDeps = () => {
2449
+ if (!sync) throw new Error("La sincronización con Drive no está configurada.");
2450
+ return { drive: driveOps(http), cms: sync.cms, writer: sync.writer, links: sync.links, ops: sync.ops, store, describe, log };
2451
+ };
2452
+ const requireConnection = async () => {
2453
+ if (!await connected()) throw new DriveInputError("Conecta Google Drive en Ajustes antes de sincronizar.");
2454
+ };
2455
+ const asInput = async (work) => {
2456
+ try {
2457
+ return await work();
2458
+ } catch (err) {
2459
+ if (err instanceof DriveSyncError) throw new DriveInputError(err.message, err.status);
2460
+ throw err;
2461
+ }
2462
+ };
2463
+ const runCycle = async ({ full = false } = {}) => await connected() ? runSync(syncDeps(), { full }) : null;
2464
+ const schedule = debounce(() => runCycle(), 5e3, (err) => log.warn(`[drive] el ciclo de sincronización falló: ${err instanceof Error ? err.message : String(err)}`));
2465
+ return {
2466
+ http: () => http,
2467
+ connected,
2468
+ runCycle,
2469
+ /** Marca para sincronizar y programa el ciclo. */
2470
+ async mark(cmsType, cmsId) {
2471
+ await requireConnection();
2472
+ await asInput(() => mark(syncDeps(), cmsType, cmsId));
2473
+ schedule();
2474
+ },
2475
+ async unmark(cmsType, cmsId) {
2476
+ await asInput(() => unmark(syncDeps(), cmsType, cmsId));
2477
+ },
2478
+ /** «Sincronizar ahora»; con `full`, «Revisión completa»: compara todo lo vinculado contra Drive. */
2479
+ async syncNow(full = false) {
2480
+ await requireConnection();
2481
+ return runCycle({ full });
2482
+ },
2483
+ /** Lifecycle de upload: encola si el elemento está (o estaba) en lo sincronizado. */
2484
+ async track(cmsType, cmsId) {
2485
+ if (isEcho() || !sync || !await connected()) return;
2486
+ const deps = syncDeps();
2487
+ if (await deps.links.find(cmsType, cmsId) || await isSynced(deps, cmsType, cmsId)) {
2488
+ await deps.ops.push(cmsType, cmsId);
2489
+ schedule();
2490
+ }
2491
+ },
2492
+ async linkStatuses() {
2493
+ const isConnected = await connected();
2494
+ if (!isConnected || !sync) return { connected: false, items: {}, lastRun: null, pending: 0 };
2495
+ const items = {};
2496
+ const deps = syncDeps();
2497
+ for (const l of await sync.links.all()) {
2498
+ items[`${l.cmsType}:${l.cmsId}`] = { status: l.status, root: l.root, inside: await isSynced(deps, l.cmsType, l.cmsId), error: l.error };
2499
+ }
2500
+ const lastRun = await store.get({ key: LAST_RUN_KEY }) ?? null;
2501
+ return { connected: true, items, lastRun, pending: await sync.ops.count() };
2502
+ },
2503
+ /** Vista de Drive (S7.2): una carpeta en vivo con su ruta y qué hijos están sincronizados. */
2504
+ async browse(folderId, pageToken) {
2505
+ await requireConnection();
2506
+ const ops = driveOps(http);
2507
+ const id = typeof folderId === "string" && folderId !== "" ? folderId : "root";
2508
+ const page = await ops.listChildren(id, typeof pageToken === "string" && pageToken ? pageToken : void 0);
2509
+ const rootId2 = await store.get({ key: "DRIVE_ROOT_ID" }) ?? await ops.rootId();
2510
+ const isRoot = id === "root" || id === rootId2;
2511
+ const path = [];
2512
+ if (!isRoot) {
2513
+ let current = await ops.getFile(id);
2514
+ path.unshift({ id: current.id, name: current.name });
2515
+ for (let depth = 0; depth < 30 && current.parents[0] && current.parents[0] !== rootId2; depth++) {
2516
+ current = await ops.getFile(current.parents[0]);
2517
+ path.unshift({ id: current.id, name: current.name });
2518
+ }
2519
+ }
2520
+ const linked = new Set((sync ? await sync.links.all() : []).map((l) => l.driveId).filter(Boolean));
2521
+ return {
2522
+ path,
2523
+ items: page.items.map((item) => ({ ...item, synced: linked.has(item.id) })),
2524
+ nextPageToken: page.nextPageToken
2525
+ };
2526
+ },
2527
+ /** Cuántos elementos se irían a la papelera con esta carpeta, y si está sincronizada. */
2528
+ async countItem(id) {
2529
+ await requireConnection();
2530
+ if (typeof id !== "string" || !id) throw new DriveInputError("Elemento de Drive inválido.");
2531
+ const synced = !!(sync && await sync.links.findByDriveId(id));
2532
+ return { ...await driveOps(http).countTree(id), synced };
2533
+ },
2534
+ /** Eliminar desde la vista: a la papelera de Drive. Si estaba sincronizado, el siguiente ciclo lo borra del CMS. */
2535
+ async trashItem(id) {
2536
+ await requireConnection();
2537
+ if (typeof id !== "string" || !id || id === "root") throw new DriveInputError("Elemento de Drive inválido.");
2538
+ await driveOps(http).trash(id);
2539
+ schedule();
2540
+ },
2541
+ /** El panel avisa que un archivo salió de un template (S7.3). */
2542
+ async setOrigin(fileId, documentId, name2) {
2543
+ if (typeof fileId !== "number" || typeof documentId !== "string" || typeof name2 !== "string" || !name2.trim()) {
2544
+ throw new DriveInputError("Origen del template inválido.");
2545
+ }
2546
+ const origins = await store.get({ key: DRIVE_ORIGINS_KEY }) ?? {};
2547
+ await store.set({ key: DRIVE_ORIGINS_KEY, value: { ...origins, [String(fileId)]: { documentId, name: name2.trim() } } });
2548
+ },
2549
+ /** Guarda el cliente del JSON de Google. Un cliente nuevo invalida la conexión anterior. */
2550
+ async saveClient(json, redirectUri) {
2551
+ const parsed = parseClientConfig(json, typeof redirectUri === "string" ? redirectUri : "");
2552
+ if ("message" in parsed) throw new DriveInputError(parsed.message);
2553
+ const previous = await client();
2554
+ if (previous && previous.clientId !== parsed.client.clientId) await forgetToken();
2555
+ await store.set({ key: DRIVE_CLIENT_KEY, value: storedClient(parsed.client) });
2556
+ http.reset();
2557
+ },
2558
+ async removeClient() {
2559
+ await forgetToken();
2560
+ await store.delete({ key: DRIVE_CLIENT_KEY });
2561
+ },
2562
+ async authorizationUrl(redirectUri) {
2563
+ const c = await clientFor(redirectUri);
2564
+ return authUrl(c, redirectUri, await newState(store));
2565
+ },
2566
+ async exchange({ code, state, redirectUri }) {
2567
+ if (!await checkState(store, state) || typeof code !== "string" || code === "") {
2568
+ throw new DriveInputError("La conexión no se pudo verificar. Intenta de nuevo.");
2569
+ }
2570
+ const c = await clientFor(redirectUri);
2571
+ let refreshToken;
2572
+ try {
2573
+ ({ refreshToken } = await exchangeCode({ client: c, code, redirectUri, fetch: fetchFn }));
2574
+ } catch (err) {
2575
+ if (err instanceof DriveAuthError) throw new DriveInputError(err.message);
2576
+ throw err;
2577
+ }
2578
+ await store.set({ key: DRIVE_TOKEN_KEY, value: { refreshToken, email: null } });
2579
+ http.reset();
2580
+ const { email } = await http.about();
2581
+ await store.set({ key: DRIVE_TOKEN_KEY, value: { refreshToken, email } });
2582
+ },
2583
+ disconnect: forgetToken,
2584
+ async status() {
2585
+ const pub = publicClient(await store.get({ key: DRIVE_CLIENT_KEY }));
2586
+ const t = await token();
2587
+ if (!pub.configured || !t?.refreshToken) return { client: pub, connection: NOT_CONNECTED, sync: null };
2588
+ const syncState = async () => {
2589
+ if (!sync) return null;
2590
+ const failing = (await sync.links.all()).filter((l) => (l.status === "error" || l.status === "skipped") && l.error);
2591
+ return {
2592
+ lastRun: await store.get({ key: LAST_RUN_KEY }) ?? null,
2593
+ pending: await sync.ops.count(),
2594
+ errors: failing.slice(0, 20).map((l) => ({ key: `${l.cmsType}:${l.cmsId}`, name: l.name ?? `${l.cmsType === "file" ? "Archivo" : "Carpeta"} ${l.cmsId}`, error: l.error }))
2595
+ };
2596
+ };
2597
+ try {
2598
+ const about = await http.about();
2599
+ return {
2600
+ client: pub,
2601
+ connection: { connected: true, email: about.email, usage: about.usage, limit: about.limit, error: null },
2602
+ sync: await syncState()
2603
+ };
2604
+ } catch (err) {
2605
+ if (err instanceof DriveAuthError || err instanceof DriveNotConnectedError) {
2606
+ return { client: pub, connection: { ...NOT_CONNECTED, email: t.email, error: REVOKED }, sync: null };
2607
+ }
2608
+ return { client: pub, connection: { ...NOT_CONNECTED, connected: true, email: t.email, error: err.message }, sync: await syncState() };
2609
+ }
2610
+ }
2611
+ };
2612
+ };
2613
+ const drive$1 = ({ strapi }) => createDriveService({
2614
+ store: strapi.store({ type: "plugin", name: "strapi-content-generator-gemini" }),
2615
+ fetch: (...args) => fetch(...args),
2616
+ log: strapi.log,
2617
+ sync: {
2618
+ cms: strapiCms(strapi),
2619
+ writer: strapiCmsWriter(strapi),
2620
+ links: strapiLinks(strapi),
2621
+ ops: strapiOps(strapi),
2622
+ serverUrl: String(strapi.config.get("server.url") ?? "").replace(/\/+$/, "")
2623
+ }
2624
+ });
2625
+ const fail = (ctx, status, name2, message) => {
2626
+ ctx.status = status;
2627
+ ctx.body = { data: null, error: { status, name: name2, message } };
2628
+ };
2629
+ const drive = ({ strapi }) => {
2630
+ const svc = () => strapi.plugin("strapi-content-generator-gemini").service("drive");
2631
+ const run = async (ctx, work) => {
2632
+ try {
2633
+ ctx.body = await work() ?? { ok: true };
2634
+ ctx.status = 200;
2635
+ } catch (err) {
2636
+ if (err instanceof DriveInputError) return fail(ctx, err.status, err.status === 409 ? "ConflictError" : "ValidationError", err.message);
2637
+ strapi.log.error(`[drive] ${err instanceof Error ? err.message : String(err)}`);
2638
+ return fail(ctx, 500, "InternalServerError", "No se pudo completar la operación con Google Drive.");
2639
+ }
2640
+ };
2641
+ const body = (ctx) => ctx.request.body ?? {};
2642
+ const target = (raw) => {
2643
+ const cmsId = Number(raw.cmsId);
2644
+ if (raw.cmsType !== "file" && raw.cmsType !== "folder" || !Number.isInteger(cmsId) || cmsId <= 0) {
2645
+ throw new DriveInputError("Elemento de la Biblioteca inválido.");
2646
+ }
2647
+ return { cmsType: raw.cmsType, cmsId };
2648
+ };
2649
+ return {
2650
+ status: (ctx) => run(ctx, () => svc().status()),
2651
+ saveClient: (ctx) => run(ctx, () => svc().saveClient(body(ctx).json, body(ctx).redirectUri).then(() => void 0)),
2652
+ removeClient: (ctx) => run(ctx, () => svc().removeClient().then(() => void 0)),
2653
+ authorizationUrl: (ctx) => run(ctx, async () => ({ url: await svc().authorizationUrl(ctx.query.redirectUri) })),
2654
+ exchange: (ctx) => run(ctx, () => svc().exchange(body(ctx)).then(() => void 0)),
2655
+ disconnect: (ctx) => run(ctx, () => svc().disconnect().then(() => void 0)),
2656
+ links: (ctx) => run(ctx, () => svc().linkStatuses()),
2657
+ mark: (ctx) => run(ctx, async () => {
2658
+ const { cmsType, cmsId } = target(body(ctx));
2659
+ await svc().mark(cmsType, cmsId);
2660
+ }),
2661
+ unmark: (ctx) => run(ctx, async () => {
2662
+ const { cmsType, cmsId } = target(ctx.params ?? {});
2663
+ await svc().unmark(cmsType, cmsId);
2664
+ }),
2665
+ syncNow: (ctx) => run(ctx, () => svc().syncNow(body(ctx).full === true)),
2666
+ browse: (ctx) => run(ctx, () => svc().browse(ctx.params?.id, ctx.query.pageToken)),
2667
+ count: (ctx) => run(ctx, () => svc().countItem(ctx.params?.id)),
2668
+ trash: (ctx) => run(ctx, () => svc().trashItem(ctx.params?.id).then(() => void 0)),
2669
+ origin: (ctx) => run(ctx, () => svc().setOrigin(body(ctx).fileId, body(ctx).templateDocumentId, body(ctx).templateName).then(() => void 0))
2670
+ };
2671
+ };
1325
2672
  const controllers = {
1326
2673
  controller,
1327
2674
  config: config$1,
1328
2675
  "image-studio": imageStudio,
1329
2676
  templates,
2677
+ drive,
1330
2678
  articles
1331
2679
  };
1332
2680
  const middlewares = {};
@@ -1375,7 +2723,21 @@ const admin = [
1375
2723
  { method: "POST", path: "/template-folders/delete", handler: "templates.deleteFolder", config },
1376
2724
  { method: "GET", path: "/config", handler: "config.get", config },
1377
2725
  { method: "POST", path: "/config", handler: "config.set", config },
1378
- { method: "DELETE", path: "/config/vertex", handler: "config.removeVertex", config }
2726
+ { method: "DELETE", path: "/config/vertex", handler: "config.removeVertex", config },
2727
+ { method: "GET", path: "/drive/status", handler: "drive.status", config },
2728
+ { method: "POST", path: "/drive/client", handler: "drive.saveClient", config },
2729
+ { method: "DELETE", path: "/drive/client", handler: "drive.removeClient", config },
2730
+ { method: "GET", path: "/drive/oauth/url", handler: "drive.authorizationUrl", config },
2731
+ { method: "POST", path: "/drive/oauth/exchange", handler: "drive.exchange", config },
2732
+ { method: "DELETE", path: "/drive/oauth", handler: "drive.disconnect", config },
2733
+ { method: "GET", path: "/drive/links", handler: "drive.links", config },
2734
+ { method: "POST", path: "/drive/links", handler: "drive.mark", config },
2735
+ { method: "DELETE", path: "/drive/links/:cmsType/:cmsId", handler: "drive.unmark", config },
2736
+ { method: "POST", path: "/drive/sync", handler: "drive.syncNow", config },
2737
+ { method: "POST", path: "/drive/origin", handler: "drive.origin", config },
2738
+ { method: "GET", path: "/drive/folders/:id", handler: "drive.browse", config },
2739
+ { method: "GET", path: "/drive/items/:id/count", handler: "drive.count", config },
2740
+ { method: "DELETE", path: "/drive/items/:id", handler: "drive.trash", config }
1379
2741
  ];
1380
2742
  const contentApi = [];
1381
2743
  const routes = {
@@ -2882,13 +4244,13 @@ function speedometer(samplesCount, min) {
2882
4244
  let firstSampleTS;
2883
4245
  min = min !== void 0 ? min : 1e3;
2884
4246
  return function push(chunkLength) {
2885
- const now = Date.now();
4247
+ const now2 = Date.now();
2886
4248
  const startedAt = timestamps[tail];
2887
4249
  if (!firstSampleTS) {
2888
- firstSampleTS = now;
4250
+ firstSampleTS = now2;
2889
4251
  }
2890
4252
  bytes[head] = chunkLength;
2891
- timestamps[head] = now;
4253
+ timestamps[head] = now2;
2892
4254
  let i = tail;
2893
4255
  let bytesCount = 0;
2894
4256
  while (i !== head) {
@@ -2899,10 +4261,10 @@ function speedometer(samplesCount, min) {
2899
4261
  if (head === tail) {
2900
4262
  tail = (tail + 1) % samplesCount;
2901
4263
  }
2902
- if (now - firstSampleTS < min) {
4264
+ if (now2 - firstSampleTS < min) {
2903
4265
  return;
2904
4266
  }
2905
- const passed = startedAt && now - startedAt;
4267
+ const passed = startedAt && now2 - startedAt;
2906
4268
  return passed ? Math.round(bytesCount * 1e3 / passed) : void 0;
2907
4269
  };
2908
4270
  }
@@ -2911,8 +4273,8 @@ function throttle(fn, freq) {
2911
4273
  let threshold = 1e3 / freq;
2912
4274
  let lastArgs;
2913
4275
  let timer;
2914
- const invoke = (args, now = Date.now()) => {
2915
- timestamp = now;
4276
+ const invoke = (args, now2 = Date.now()) => {
4277
+ timestamp = now2;
2916
4278
  lastArgs = null;
2917
4279
  if (timer) {
2918
4280
  clearTimeout(timer);
@@ -2921,10 +4283,10 @@ function throttle(fn, freq) {
2921
4283
  fn(...args);
2922
4284
  };
2923
4285
  const throttled = (...args) => {
2924
- const now = Date.now();
2925
- const passed = now - timestamp;
4286
+ const now2 = Date.now();
4287
+ const passed = now2 - timestamp;
2926
4288
  if (passed >= threshold) {
2927
- invoke(args, now);
4289
+ invoke(args, now2);
2928
4290
  } else {
2929
4291
  lastArgs = args;
2930
4292
  if (!timer) {
@@ -4584,8 +5946,8 @@ axios.VERSION = VERSION$1;
4584
5946
  axios.toFormData = toFormData$1;
4585
5947
  axios.AxiosError = AxiosError$1;
4586
5948
  axios.Cancel = axios.CanceledError;
4587
- axios.all = function all(promises) {
4588
- return Promise.all(promises);
5949
+ axios.all = function all(promises2) {
5950
+ return Promise.all(promises2);
4589
5951
  };
4590
5952
  axios.spread = spread$1;
4591
5953
  axios.isAxiosError = isAxiosError$1;
@@ -4973,6 +6335,7 @@ const services = {
4973
6335
  service,
4974
6336
  "image-studio": imageStudio$1,
4975
6337
  templates: templates$1,
6338
+ drive: drive$1,
4976
6339
  articles: articles$1
4977
6340
  };
4978
6341
  const index = {