@interware/content-generator 0.7.2 → 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 (45) hide show
  1. package/dist/_chunks/{App-CTh6owSM.mjs → App-DD1MzaWm.mjs} +2 -2
  2. package/dist/_chunks/{App-ugsCmlsC.js → App-DspaMfyW.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-Umf3GODW.mjs → ImageStudioTabs-DwaLxRjl.mjs} +308 -16
  4. package/dist/_chunks/{ImageStudioTabs-BFEF9ggK.js → ImageStudioTabs-uXSBkFeu.js} +306 -14
  5. package/dist/_chunks/{Settings-CdGW2O8h.mjs → Settings-C6TbUrgU.mjs} +170 -2
  6. package/dist/_chunks/{Settings-Sk78JUBS.js → Settings-ps0o7-fC.js} +169 -1
  7. package/dist/_chunks/{StudioApp-CrGn-9Gg.mjs → StudioApp-B5I1OfGw.mjs} +2 -2
  8. package/dist/_chunks/{StudioApp-DoC0z4eh.js → StudioApp-B7kGpqqo.js} +2 -2
  9. package/dist/_chunks/{index-C2s9Jhcc.js → index-BudahdfX.js} +3 -3
  10. package/dist/_chunks/{index-D6UAvh53.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/export/templateOrigin.d.ts +10 -0
  19. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  20. package/dist/server/index.js +1411 -49
  21. package/dist/server/index.mjs +1409 -47
  22. package/dist/server/src/bootstrap.d.ts +2 -0
  23. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  24. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  25. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  26. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  27. package/dist/server/src/content-types/index.d.ts +106 -0
  28. package/dist/server/src/controllers/drive.d.ts +30 -0
  29. package/dist/server/src/controllers/index.d.ts +130 -0
  30. package/dist/server/src/drive/client.d.ts +32 -0
  31. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  32. package/dist/server/src/drive/common.d.ts +6 -0
  33. package/dist/server/src/drive/fakes.d.ts +104 -0
  34. package/dist/server/src/drive/files.d.ts +136 -0
  35. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  36. package/dist/server/src/drive/links.d.ts +23 -0
  37. package/dist/server/src/drive/model.d.ts +99 -0
  38. package/dist/server/src/drive/oauth.d.ts +47 -0
  39. package/dist/server/src/drive/pull.d.ts +23 -0
  40. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  41. package/dist/server/src/drive/sync.d.ts +40 -0
  42. package/dist/server/src/index.d.ts +294 -0
  43. package/dist/server/src/services/drive.d.ts +141 -0
  44. package/dist/server/src/services/index.d.ts +55 -0
  45. 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(" ");
@@ -486,7 +583,7 @@ const parseEditInput = (fields, files) => {
486
583
  aspectRatio: model.aspectRatios.includes(str(fields.aspectRatio)) ? str(fields.aspectRatio) : void 0
487
584
  };
488
585
  };
489
- const fail$3 = (ctx, status, name2, message) => {
586
+ const fail$4 = (ctx, status, name2, message) => {
490
587
  ctx.status = status;
491
588
  ctx.body = { data: null, error: { status, name: name2, message } };
492
589
  };
@@ -496,13 +593,13 @@ const imageStudio = ({ strapi }) => {
496
593
  try {
497
594
  ctx.body = await work();
498
595
  } catch (err) {
499
- 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);
500
597
  if (err instanceof BlockedError) {
501
598
  strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
502
- return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
599
+ return fail$4(ctx, 422, "UnprocessableEntityError", err.message);
503
600
  }
504
601
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
505
- 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.");
506
603
  }
507
604
  };
508
605
  return {
@@ -711,11 +808,11 @@ const ARTICLE_SCHEMA = {
711
808
  const ZONA = "America/Mexico_City";
712
809
  const UN_ANIO_MS = 365 * 24 * 60 * 60 * 1e3;
713
810
  const fechaEnMexico = (date) => new Intl.DateTimeFormat("en-CA", { timeZone: ZONA, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
714
- const parseScheduleAt = (input, now) => {
811
+ const parseScheduleAt = (input, now2) => {
715
812
  const date = typeof input === "string" ? new Date(input) : /* @__PURE__ */ new Date(NaN);
716
813
  if (Number.isNaN(date.getTime())) throw new InputError("La fecha y hora no son válidas.");
717
- if (date.getTime() <= now.getTime()) throw new InputError("Elige una fecha y hora futuras.");
718
- 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.");
719
816
  return date;
720
817
  };
721
818
  const estadoDe = ({ publicado, programada }) => publicado ? "publicado" : programada ? "programado" : "borrador";
@@ -761,7 +858,7 @@ const toDetail = (d, publicado) => ({
761
858
  keywords: (d.keywords ?? []).map((k) => k.name ?? "").filter(Boolean),
762
859
  fuentes: Array.isArray(d.fuentes) ? d.fuentes : []
763
860
  });
764
- 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() }) => {
765
862
  const draftOf = async (documentId, populate = DETAIL_POPULATE) => {
766
863
  const d = await articulos.findOne({ documentId, status: "draft", populate });
767
864
  if (!d) throw new NotFoundError("El artículo no existe.");
@@ -865,12 +962,12 @@ const createArticles = ({ articulos, categorias, getModels, now = () => /* @__PU
865
962
  async publish(documentId) {
866
963
  const d = await draftOf(documentId);
867
964
  if (!d.image) throw new InputError("Elige una imagen antes de publicar.");
868
- 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 } });
869
966
  await articulos.publish({ documentId });
870
967
  return detail(documentId);
871
968
  },
872
969
  async schedule(documentId, at) {
873
- const date = parseScheduleAt(at, now());
970
+ const date = parseScheduleAt(at, now2());
874
971
  const d = await draftOf(documentId);
875
972
  if (!d.image) throw new InputError("Elige una imagen antes de programar.");
876
973
  if (await isPublished(documentId)) throw new InputError("El artículo ya está publicado.");
@@ -886,7 +983,7 @@ const createArticles = ({ articulos, categorias, getModels, now = () => /* @__PU
886
983
  async publishDue() {
887
984
  const due = await articulos.findMany({
888
985
  status: "draft",
889
- filters: { publicacion_programada: { $notNull: true, $lte: now().toISOString() } },
986
+ filters: { publicacion_programada: { $notNull: true, $lte: now2().toISOString() } },
890
987
  fields: ["publicacion_programada"],
891
988
  populate: { image: { fields: ["id"] } }
892
989
  });
@@ -928,7 +1025,7 @@ const articles$1 = ({ strapi }) => createArticles({
928
1025
  };
929
1026
  })()
930
1027
  });
931
- const fail$2 = (ctx, status, name2, message) => {
1028
+ const fail$3 = (ctx, status, name2, message) => {
932
1029
  ctx.status = status;
933
1030
  ctx.body = { data: null, error: { status, name: name2, message } };
934
1031
  };
@@ -939,11 +1036,11 @@ const articles = ({ strapi }) => {
939
1036
  try {
940
1037
  ctx.body = await work();
941
1038
  } catch (err) {
942
- if (err instanceof InputError) return fail$2(ctx, 400, "BadRequestError", err.message);
943
- if (err instanceof NotFoundError) return fail$2(ctx, 404, "NotFoundError", err.message);
944
- 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);
945
1042
  strapi.log.error(`[articles] ${err instanceof Error ? err.message : String(err)}`);
946
- 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.");
947
1044
  }
948
1045
  };
949
1046
  return {
@@ -1109,7 +1206,7 @@ class TemplateInputError extends Error {
1109
1206
  }
1110
1207
  const LAYER_TYPES = ["text", "list", "quote", "rect", "shape", "badge", "logo", "image", "icon", "avatar", "qr"];
1111
1208
  const MAX_LAYERS = 200;
1112
- const fail$1 = (message) => {
1209
+ const fail$2 = (message) => {
1113
1210
  throw new TemplateInputError(message);
1114
1211
  };
1115
1212
  const hasDataUrl = (value) => {
@@ -1120,26 +1217,26 @@ const hasDataUrl = (value) => {
1120
1217
  };
1121
1218
  const name = (v) => {
1122
1219
  const n = typeof v === "string" ? v.trim() : "";
1123
- 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.");
1124
1221
  return n;
1125
1222
  };
1126
1223
  const size = (v) => {
1127
- 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.");
1128
1225
  return v;
1129
1226
  };
1130
1227
  const nonNegative = (v, label) => {
1131
- 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.`);
1132
1229
  return v;
1133
1230
  };
1134
1231
  const layers = (v) => {
1135
- 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}.`);
1136
1233
  v.forEach((l, i) => {
1137
1234
  const layer = l ?? {};
1138
- if (typeof layer.id !== "string" || !layer.id) fail$1(`La capa ${i + 1} no tiene id.`);
1139
- if (!LAYER_TYPES.includes(layer.type)) fail$1(`La capa ${i + 1} tiene un tipo desconocido.`);
1140
- 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.`);
1141
1238
  });
1142
- 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.");
1143
1240
  return v;
1144
1241
  };
1145
1242
  const text = (v, fallback) => typeof v === "string" ? v : fallback;
@@ -1169,7 +1266,7 @@ const validateTemplatePatch = (input) => {
1169
1266
  if ("safeMargin" in t) out.safeMargin = nonNegative(t.safeMargin, "El margen seguro");
1170
1267
  if ("gridUnit" in t) out.gridUnit = nonNegative(t.gridUnit, "La cuadrícula");
1171
1268
  if ("layers" in t) out.layers = layers(t.layers);
1172
- if (!Object.keys(out).length) fail$1("Nada que actualizar.");
1269
+ if (!Object.keys(out).length) fail$2("Nada que actualizar.");
1173
1270
  return out;
1174
1271
  };
1175
1272
  const TEMPLATE_UID = "plugin::strapi-content-generator-gemini.template";
@@ -1285,7 +1382,7 @@ const templates$1 = ({ strapi }) => {
1285
1382
  { get: (p) => store().get(p), set: (p) => store().set(p) }
1286
1383
  );
1287
1384
  };
1288
- const fail = (ctx, status, name2, message) => {
1385
+ const fail$1 = (ctx, status, name2, message) => {
1289
1386
  ctx.status = status;
1290
1387
  ctx.body = { data: null, error: { status, name: name2, message } };
1291
1388
  };
@@ -1296,10 +1393,10 @@ const templates = ({ strapi }) => {
1296
1393
  ctx.body = await work();
1297
1394
  ctx.status = status;
1298
1395
  } catch (err) {
1299
- if (err instanceof TemplateInputError) return fail(ctx, 400, "BadRequestError", err.message);
1300
- 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);
1301
1398
  strapi.log.error(`[templates] ${err instanceof Error ? err.message : String(err)}`);
1302
- 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.");
1303
1400
  }
1304
1401
  };
1305
1402
  const body = (ctx) => ctx.request.body ?? {};
@@ -1323,11 +1420,1261 @@ const templates = ({ strapi }) => {
1323
1420
  deleteFolder: (ctx) => run(ctx, async () => ({ folders: await svc().deleteFolder(body(ctx).path) }))
1324
1421
  };
1325
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
+ };
1326
2672
  const controllers = {
1327
2673
  controller,
1328
2674
  config: config$1,
1329
2675
  "image-studio": imageStudio,
1330
2676
  templates,
2677
+ drive,
1331
2678
  articles
1332
2679
  };
1333
2680
  const middlewares = {};
@@ -1376,7 +2723,21 @@ const admin = [
1376
2723
  { method: "POST", path: "/template-folders/delete", handler: "templates.deleteFolder", config },
1377
2724
  { method: "GET", path: "/config", handler: "config.get", config },
1378
2725
  { method: "POST", path: "/config", handler: "config.set", config },
1379
- { 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 }
1380
2741
  ];
1381
2742
  const contentApi = [];
1382
2743
  const routes = {
@@ -2883,13 +4244,13 @@ function speedometer(samplesCount, min) {
2883
4244
  let firstSampleTS;
2884
4245
  min = min !== void 0 ? min : 1e3;
2885
4246
  return function push(chunkLength) {
2886
- const now = Date.now();
4247
+ const now2 = Date.now();
2887
4248
  const startedAt = timestamps[tail];
2888
4249
  if (!firstSampleTS) {
2889
- firstSampleTS = now;
4250
+ firstSampleTS = now2;
2890
4251
  }
2891
4252
  bytes[head] = chunkLength;
2892
- timestamps[head] = now;
4253
+ timestamps[head] = now2;
2893
4254
  let i = tail;
2894
4255
  let bytesCount = 0;
2895
4256
  while (i !== head) {
@@ -2900,10 +4261,10 @@ function speedometer(samplesCount, min) {
2900
4261
  if (head === tail) {
2901
4262
  tail = (tail + 1) % samplesCount;
2902
4263
  }
2903
- if (now - firstSampleTS < min) {
4264
+ if (now2 - firstSampleTS < min) {
2904
4265
  return;
2905
4266
  }
2906
- const passed = startedAt && now - startedAt;
4267
+ const passed = startedAt && now2 - startedAt;
2907
4268
  return passed ? Math.round(bytesCount * 1e3 / passed) : void 0;
2908
4269
  };
2909
4270
  }
@@ -2912,8 +4273,8 @@ function throttle(fn, freq) {
2912
4273
  let threshold = 1e3 / freq;
2913
4274
  let lastArgs;
2914
4275
  let timer;
2915
- const invoke = (args, now = Date.now()) => {
2916
- timestamp = now;
4276
+ const invoke = (args, now2 = Date.now()) => {
4277
+ timestamp = now2;
2917
4278
  lastArgs = null;
2918
4279
  if (timer) {
2919
4280
  clearTimeout(timer);
@@ -2922,10 +4283,10 @@ function throttle(fn, freq) {
2922
4283
  fn(...args);
2923
4284
  };
2924
4285
  const throttled = (...args) => {
2925
- const now = Date.now();
2926
- const passed = now - timestamp;
4286
+ const now2 = Date.now();
4287
+ const passed = now2 - timestamp;
2927
4288
  if (passed >= threshold) {
2928
- invoke(args, now);
4289
+ invoke(args, now2);
2929
4290
  } else {
2930
4291
  lastArgs = args;
2931
4292
  if (!timer) {
@@ -4585,8 +5946,8 @@ axios.VERSION = VERSION$1;
4585
5946
  axios.toFormData = toFormData$1;
4586
5947
  axios.AxiosError = AxiosError$1;
4587
5948
  axios.Cancel = axios.CanceledError;
4588
- axios.all = function all(promises) {
4589
- return Promise.all(promises);
5949
+ axios.all = function all(promises2) {
5950
+ return Promise.all(promises2);
4590
5951
  };
4591
5952
  axios.spread = spread$1;
4592
5953
  axios.isAxiosError = isAxiosError$1;
@@ -4974,6 +6335,7 @@ const services = {
4974
6335
  service,
4975
6336
  "image-studio": imageStudio$1,
4976
6337
  templates: templates$1,
6338
+ drive: drive$1,
4977
6339
  articles: articles$1
4978
6340
  };
4979
6341
  const index = {