cross-seed 7.0.0-1 → 7.0.0-11

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 (287) hide show
  1. package/dist/Result.d.ts +27 -0
  2. package/dist/Result.js +64 -0
  3. package/dist/Result.js.map +1 -0
  4. package/dist/action.d.ts +34 -0
  5. package/dist/action.js +694 -0
  6. package/dist/action.js.map +1 -0
  7. package/dist/arr.d.ts +31 -0
  8. package/dist/arr.js +267 -0
  9. package/dist/arr.js.map +1 -0
  10. package/dist/auth.d.ts +3 -0
  11. package/dist/auth.js +28 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/clients/Deluge.d.ts +153 -0
  14. package/dist/clients/Deluge.js +699 -0
  15. package/dist/clients/Deluge.js.map +1 -0
  16. package/dist/clients/QBittorrent.d.ts +218 -0
  17. package/dist/clients/QBittorrent.js +786 -0
  18. package/dist/clients/QBittorrent.js.map +1 -0
  19. package/dist/clients/RTorrent.d.ts +43 -0
  20. package/dist/clients/RTorrent.js +658 -0
  21. package/dist/clients/RTorrent.js.map +1 -0
  22. package/dist/clients/TorrentClient.d.ts +108 -0
  23. package/dist/clients/TorrentClient.js +342 -0
  24. package/dist/clients/TorrentClient.js.map +1 -0
  25. package/dist/clients/Transmission.d.ts +43 -0
  26. package/dist/clients/Transmission.js +405 -0
  27. package/dist/clients/Transmission.js.map +1 -0
  28. package/dist/cmd.d.ts +2 -0
  29. package/dist/cmd.js +128 -0
  30. package/dist/cmd.js.map +1 -0
  31. package/dist/configSchema.d.ts +1 -0
  32. package/dist/configSchema.js +2 -0
  33. package/dist/configSchema.js.map +1 -0
  34. package/dist/configuration.d.ts +63 -0
  35. package/dist/configuration.js +321 -0
  36. package/dist/configuration.js.map +1 -0
  37. package/dist/constants.d.ts +108 -0
  38. package/dist/constants.js +251 -0
  39. package/dist/constants.js.map +1 -0
  40. package/dist/dataFiles.d.ts +8 -0
  41. package/dist/dataFiles.js +223 -0
  42. package/dist/dataFiles.js.map +1 -0
  43. package/dist/db.d.ts +14 -0
  44. package/dist/db.js +287 -0
  45. package/dist/db.js.map +1 -0
  46. package/dist/dbConfig.d.ts +4 -0
  47. package/dist/dbConfig.js +67 -0
  48. package/dist/dbConfig.js.map +1 -0
  49. package/dist/decide.d.ts +25 -0
  50. package/dist/decide.js +553 -0
  51. package/dist/decide.js.map +1 -0
  52. package/dist/diagnostics/db.d.ts +21 -0
  53. package/dist/diagnostics/db.js +107 -0
  54. package/dist/diagnostics/db.js.map +1 -0
  55. package/dist/diff.d.ts +1 -0
  56. package/dist/diff.js +24 -0
  57. package/dist/diff.js.map +1 -0
  58. package/dist/errors.d.ts +3 -0
  59. package/dist/errors.js +7 -0
  60. package/dist/errors.js.map +1 -0
  61. package/dist/indexers.d.ts +105 -0
  62. package/dist/indexers.js +248 -0
  63. package/dist/indexers.js.map +1 -0
  64. package/dist/inject.d.ts +2 -0
  65. package/dist/inject.js +594 -0
  66. package/dist/inject.js.map +1 -0
  67. package/dist/jobs.d.ts +29 -0
  68. package/dist/jobs.js +151 -0
  69. package/dist/jobs.js.map +1 -0
  70. package/dist/logger.d.ts +29 -0
  71. package/dist/logger.js +157 -0
  72. package/dist/logger.js.map +1 -0
  73. package/dist/migrations/00-initialSchema.d.ts +9 -0
  74. package/dist/migrations/00-initialSchema.js +30 -0
  75. package/dist/migrations/00-initialSchema.js.map +1 -0
  76. package/dist/migrations/01-jobs.d.ts +9 -0
  77. package/dist/migrations/01-jobs.js +12 -0
  78. package/dist/migrations/01-jobs.js.map +1 -0
  79. package/dist/migrations/02-timestamps.d.ts +9 -0
  80. package/dist/migrations/02-timestamps.js +21 -0
  81. package/dist/migrations/02-timestamps.js.map +1 -0
  82. package/dist/migrations/03-rateLimits.d.ts +9 -0
  83. package/dist/migrations/03-rateLimits.js +14 -0
  84. package/dist/migrations/03-rateLimits.js.map +1 -0
  85. package/dist/migrations/04-auth.d.ts +9 -0
  86. package/dist/migrations/04-auth.js +13 -0
  87. package/dist/migrations/04-auth.js.map +1 -0
  88. package/dist/migrations/05-caps.d.ts +9 -0
  89. package/dist/migrations/05-caps.js +16 -0
  90. package/dist/migrations/05-caps.js.map +1 -0
  91. package/dist/migrations/06-uniqueDecisions.d.ts +9 -0
  92. package/dist/migrations/06-uniqueDecisions.js +29 -0
  93. package/dist/migrations/06-uniqueDecisions.js.map +1 -0
  94. package/dist/migrations/07-limits.d.ts +9 -0
  95. package/dist/migrations/07-limits.js +12 -0
  96. package/dist/migrations/07-limits.js.map +1 -0
  97. package/dist/migrations/08-rss.d.ts +9 -0
  98. package/dist/migrations/08-rss.js +15 -0
  99. package/dist/migrations/08-rss.js.map +1 -0
  100. package/dist/migrations/09-clientAndDataSearchees.d.ts +9 -0
  101. package/dist/migrations/09-clientAndDataSearchees.js +34 -0
  102. package/dist/migrations/09-clientAndDataSearchees.js.map +1 -0
  103. package/dist/migrations/10-indexerNameAudioBookCaps.d.ts +9 -0
  104. package/dist/migrations/10-indexerNameAudioBookCaps.js +18 -0
  105. package/dist/migrations/10-indexerNameAudioBookCaps.js.map +1 -0
  106. package/dist/migrations/11-trackers.d.ts +9 -0
  107. package/dist/migrations/11-trackers.js +38 -0
  108. package/dist/migrations/11-trackers.js.map +1 -0
  109. package/dist/migrations/12-user-auth.d.ts +9 -0
  110. package/dist/migrations/12-user-auth.js +22 -0
  111. package/dist/migrations/12-user-auth.js.map +1 -0
  112. package/dist/migrations/13-settings.d.ts +9 -0
  113. package/dist/migrations/13-settings.js +23 -0
  114. package/dist/migrations/13-settings.js.map +1 -0
  115. package/dist/migrations/14-indexer-enabled-flag.d.ts +9 -0
  116. package/dist/migrations/14-indexer-enabled-flag.js +12 -0
  117. package/dist/migrations/14-indexer-enabled-flag.js.map +1 -0
  118. package/dist/migrations/15-remove-url-unique-constraint.d.ts +9 -0
  119. package/dist/migrations/15-remove-url-unique-constraint.js +14 -0
  120. package/dist/migrations/15-remove-url-unique-constraint.js.map +1 -0
  121. package/dist/migrations/16-prune-inactive-indexers.d.ts +9 -0
  122. package/dist/migrations/16-prune-inactive-indexers.js +17 -0
  123. package/dist/migrations/16-prune-inactive-indexers.js.map +1 -0
  124. package/dist/migrations/migrations.d.ts +13 -0
  125. package/dist/migrations/migrations.js +41 -0
  126. package/dist/migrations/migrations.js.map +1 -0
  127. package/dist/parseTorrent.d.ts +53 -0
  128. package/dist/parseTorrent.js +128 -0
  129. package/dist/parseTorrent.js.map +1 -0
  130. package/dist/pipeline.d.ts +41 -0
  131. package/dist/pipeline.js +578 -0
  132. package/dist/pipeline.js.map +1 -0
  133. package/dist/preFilter.d.ts +25 -0
  134. package/dist/preFilter.js +250 -0
  135. package/dist/preFilter.js.map +1 -0
  136. package/dist/problems/linking.d.ts +2 -0
  137. package/dist/problems/linking.js +80 -0
  138. package/dist/problems/linking.js.map +1 -0
  139. package/dist/problems/path.d.ts +22 -0
  140. package/dist/problems/path.js +96 -0
  141. package/dist/problems/path.js.map +1 -0
  142. package/dist/problems.d.ts +13 -0
  143. package/dist/problems.js +48 -0
  144. package/dist/problems.js.map +1 -0
  145. package/dist/pushNotifier.d.ts +19 -0
  146. package/dist/pushNotifier.js +137 -0
  147. package/dist/pushNotifier.js.map +1 -0
  148. package/dist/routes/baseApi.d.ts +2 -0
  149. package/dist/routes/baseApi.js +354 -0
  150. package/dist/routes/baseApi.js.map +1 -0
  151. package/dist/routes/indexerApi.d.ts +6 -0
  152. package/dist/routes/indexerApi.js +165 -0
  153. package/dist/routes/indexerApi.js.map +1 -0
  154. package/dist/routes/staticFrontendPlugin.d.ts +4 -0
  155. package/dist/routes/staticFrontendPlugin.js +61 -0
  156. package/dist/routes/staticFrontendPlugin.js.map +1 -0
  157. package/dist/runtimeConfig.d.ts +6 -0
  158. package/dist/runtimeConfig.js +27 -0
  159. package/dist/runtimeConfig.js.map +1 -0
  160. package/dist/searchee.d.ts +108 -0
  161. package/dist/searchee.js +690 -0
  162. package/dist/searchee.js.map +1 -0
  163. package/dist/server.d.ts +4 -0
  164. package/dist/server.js +65 -0
  165. package/dist/server.js.map +1 -0
  166. package/dist/services/indexerService.d.ts +96 -0
  167. package/dist/services/indexerService.js +287 -0
  168. package/dist/services/indexerService.js.map +1 -0
  169. package/dist/sessionCookies.d.ts +5 -0
  170. package/dist/sessionCookies.js +27 -0
  171. package/dist/sessionCookies.js.map +1 -0
  172. package/dist/startup.d.ts +25 -0
  173. package/dist/startup.js +157 -0
  174. package/dist/startup.js.map +1 -0
  175. package/dist/torrent.d.ts +69 -0
  176. package/dist/torrent.js +665 -0
  177. package/dist/torrent.js.map +1 -0
  178. package/dist/torznab.d.ts +60 -0
  179. package/dist/torznab.js +711 -0
  180. package/dist/torznab.js.map +1 -0
  181. package/dist/trpc/fastifyAdapter.d.ts +2 -0
  182. package/dist/trpc/fastifyAdapter.js +9 -0
  183. package/dist/trpc/fastifyAdapter.js.map +1 -0
  184. package/dist/trpc/index.d.ts +49 -0
  185. package/dist/trpc/index.js +53 -0
  186. package/dist/trpc/index.js.map +1 -0
  187. package/dist/trpc/routers/auth.d.ts +43 -0
  188. package/dist/trpc/routers/auth.js +116 -0
  189. package/dist/trpc/routers/auth.js.map +1 -0
  190. package/dist/trpc/routers/clients.d.ts +21 -0
  191. package/dist/trpc/routers/clients.js +65 -0
  192. package/dist/trpc/routers/clients.js.map +1 -0
  193. package/dist/trpc/routers/health.d.ts +17 -0
  194. package/dist/trpc/routers/health.js +24 -0
  195. package/dist/trpc/routers/health.js.map +1 -0
  196. package/dist/trpc/routers/index.d.ts +394 -0
  197. package/dist/trpc/routers/index.js +23 -0
  198. package/dist/trpc/routers/index.js.map +1 -0
  199. package/dist/trpc/routers/indexers.d.ts +75 -0
  200. package/dist/trpc/routers/indexers.js +79 -0
  201. package/dist/trpc/routers/indexers.js.map +1 -0
  202. package/dist/trpc/routers/jobs.d.ts +33 -0
  203. package/dist/trpc/routers/jobs.js +84 -0
  204. package/dist/trpc/routers/jobs.js.map +1 -0
  205. package/dist/trpc/routers/logs.d.ts +27 -0
  206. package/dist/trpc/routers/logs.js +91 -0
  207. package/dist/trpc/routers/logs.js.map +1 -0
  208. package/dist/trpc/routers/searchees.d.ts +51 -0
  209. package/dist/trpc/routers/searchees.js +156 -0
  210. package/dist/trpc/routers/searchees.js.map +1 -0
  211. package/dist/trpc/routers/settings.d.ts +83 -0
  212. package/dist/trpc/routers/settings.js +92 -0
  213. package/dist/trpc/routers/settings.js.map +1 -0
  214. package/dist/trpc/routers/stats.d.ts +42 -0
  215. package/dist/trpc/routers/stats.js +102 -0
  216. package/dist/trpc/routers/stats.js.map +1 -0
  217. package/dist/userAuth.d.ts +21 -0
  218. package/dist/userAuth.js +86 -0
  219. package/dist/userAuth.js.map +1 -0
  220. package/dist/utils/authUtils.d.ts +10 -0
  221. package/dist/utils/authUtils.js +24 -0
  222. package/dist/utils/authUtils.js.map +1 -0
  223. package/dist/utils/logWatcher.d.ts +28 -0
  224. package/dist/utils/logWatcher.js +229 -0
  225. package/dist/utils/logWatcher.js.map +1 -0
  226. package/dist/utils/object.d.ts +1 -0
  227. package/dist/utils/object.js +4 -0
  228. package/dist/utils/object.js.map +1 -0
  229. package/dist/utils.d.ts +172 -0
  230. package/dist/utils.js +648 -0
  231. package/dist/utils.js.map +1 -0
  232. package/dist/webui/assets/{FieldInfo-Bxj_j8SJ.js → FieldInfo-sRlPRNSK.js} +1 -1
  233. package/dist/webui/assets/{Page-C3rteCZt.js → Page-B68mlTwU.js} +1 -1
  234. package/dist/webui/assets/{array-field-DVSC6nHP.js → array-field-BCFMrvoU.js} +1 -1
  235. package/dist/webui/assets/{badge-DTZMtS0e.js → badge-C5YCxEzP.js} +1 -1
  236. package/dist/webui/assets/check-NQsw6yBl.js +1 -0
  237. package/dist/webui/assets/{chevron-down-CRy8M0kJ.js → chevron-down-8PGvFYxV.js} +1 -1
  238. package/dist/webui/assets/{clients-CW8oEZoQ.js → clients-DnVpwApe.js} +1 -1
  239. package/dist/webui/assets/{connect-YBNsnjWT.js → connect-wMg2zyz6.js} +1 -1
  240. package/dist/webui/assets/debug-BrjwiEi2.js +1 -0
  241. package/dist/webui/assets/{directories-BSK28RgR.js → directories-CHpJCWNR.js} +1 -1
  242. package/dist/webui/assets/{duration-field-C6xoSlJg.js → duration-field-DIkKt3iw.js} +1 -1
  243. package/dist/webui/assets/{general-lJJxZhH7.js → general-uZrUIxbI.js} +1 -1
  244. package/dist/webui/assets/health-_MuvAyjo.js +1 -0
  245. package/dist/webui/assets/index-B41DM2T5.css +1 -0
  246. package/dist/webui/assets/{index-C2cH1Gst.js → index-BBzHsn7u.js} +1 -1
  247. package/dist/webui/assets/{index-Bi48hI2z.js → index-Ncy0-Qo7.js} +3 -3
  248. package/dist/webui/assets/index-pKWy6v1P.js +1 -0
  249. package/dist/webui/assets/{jobs-CxmNab9w.js → jobs-B8eat0YU.js} +1 -1
  250. package/dist/webui/assets/{library-vaj2W8sE.js → library-BB0jQ8zn.js} +1 -1
  251. package/dist/webui/assets/{loader-circle-M0gu1gZ-.js → loader-circle-Bz67bJa3.js} +1 -1
  252. package/dist/webui/assets/logs-CeP28848.js +1 -0
  253. package/dist/webui/assets/{search-2R5sIdT8.js → search-BRBIrqaX.js} +1 -1
  254. package/dist/webui/assets/{select-zHgqMzLj.js → select-GZr6C6eZ.js} +1 -1
  255. package/dist/webui/assets/{select-field-BCqNLDrJ.js → select-field-CvT0SYk8.js} +1 -1
  256. package/dist/webui/assets/settings-0ZdYY8g_.js +1 -0
  257. package/dist/webui/assets/{submit-button-BtcnyggQ.js → submit-button-D7DKHqAq.js} +1 -1
  258. package/dist/webui/assets/{switch-G0W3uJVN.js → switch-BeMrf8sh.js} +1 -1
  259. package/dist/webui/assets/{switch-field-IBd9ORNq.js → switch-field-qMXHRKhx.js} +1 -1
  260. package/dist/webui/assets/{table-DvgJU7Gh.js → table-qEFWauuw.js} +1 -1
  261. package/dist/webui/assets/{test-tube-BIwmoM45.js → test-tube-DhD6uWdp.js} +1 -1
  262. package/dist/webui/assets/{text-field-DruSbGhy.js → text-field-ZnKHDUks.js} +1 -1
  263. package/dist/webui/assets/{time-BSMZjmyW.js → time-BM9K_Fbp.js} +1 -1
  264. package/dist/webui/assets/{trackers-D-OpAe63.js → trackers-BjJuAdX3.js} +1 -1
  265. package/dist/webui/assets/{use-form-validation-context-BkAfWAh0.js → use-form-validation-context-D2oA54L_.js} +1 -1
  266. package/dist/webui/assets/{use-settings-form-submit-CDRh-E9U.js → use-settings-form-submit-CXwtE1sI.js} +2 -2
  267. package/dist/webui/assets/useQuery-DD10sbzn.js +1 -0
  268. package/dist/webui/index.html +2 -2
  269. package/node_modules/@cross-seed/shared/dist/configSchema.d.ts.map +1 -1
  270. package/node_modules/@cross-seed/shared/dist/configSchema.js.map +1 -1
  271. package/node_modules/@cross-seed/shared/dist/tsconfig.tsbuildinfo +1 -1
  272. package/node_modules/@cross-seed/shared/dist/utils.d.ts +3 -0
  273. package/node_modules/@cross-seed/shared/dist/utils.d.ts.map +1 -1
  274. package/node_modules/@cross-seed/shared/dist/utils.js +11 -0
  275. package/node_modules/@cross-seed/shared/dist/utils.js.map +1 -1
  276. package/node_modules/@cross-seed/shared/package.json +3 -1
  277. package/package.json +18 -59
  278. package/LICENSE +0 -201
  279. package/README.md +0 -33
  280. package/dist/webui/assets/check-Bu3ldi63.js +0 -1
  281. package/dist/webui/assets/debug-mz8-WYZj.js +0 -1
  282. package/dist/webui/assets/health-CXbsVrie.js +0 -1
  283. package/dist/webui/assets/index-C-Ul7GNg.css +0 -1
  284. package/dist/webui/assets/index-Cc5bDmJr.js +0 -1
  285. package/dist/webui/assets/logs-Cu9RyKS0.js +0 -1
  286. package/dist/webui/assets/settings-CMYjpTbZ.js +0 -1
  287. package/dist/webui/assets/useQuery-A4Hv_4uX.js +0 -1
package/dist/action.js ADDED
@@ -0,0 +1,694 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import { copyFile, link, lstat, mkdir, readlink, rm, stat, symlink, utimes, writeFile, } from "fs/promises";
4
+ import { dirname, join, resolve } from "path";
5
+ import { getClients, shouldRecheck, } from "./clients/TorrentClient.js";
6
+ import { Action, ALL_EXTENSIONS, Decision, InjectionResult, LinkType, SaveResult, } from "./constants.js";
7
+ import { CrossSeedError } from "./errors.js";
8
+ import { Label, logger } from "./logger.js";
9
+ import { resultOf, resultOfErr } from "./Result.js";
10
+ import { getRuntimeConfig } from "./runtimeConfig.js";
11
+ import { createSearcheeFromPath, getMediaType, getRoot, getRootFolder, getSearcheeSource, } from "./searchee.js";
12
+ import { getTorrentSavePath } from "./torrent.js";
13
+ import { exists, filterAsync, findAFileWithExt, findAsync, formatAsList, getLogString, mapAsync, Mutex, notExists, verifyDir, withMutex, } from "./utils.js";
14
+ const linkDirSrcName = "linkDirSrc.cross-seed";
15
+ const linkDirDestName = "linkDirDest.cross-seed";
16
+ const clientDestName = "torrentClientDest.cross-seed";
17
+ async function linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, options) {
18
+ const availableFiles = searchee.files.slice();
19
+ const paths = decision === Decision.MATCH && options.savePath
20
+ ? newMeta.files.map((file) => [
21
+ join(options.savePath, file.path),
22
+ join(destinationDir, file.path),
23
+ ])
24
+ : newMeta.files.reduce((acc, newFile) => {
25
+ let matchedSearcheeFiles = availableFiles.filter((searcheeFile) => searcheeFile.length === newFile.length);
26
+ if (matchedSearcheeFiles.length > 1) {
27
+ matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === newFile.name);
28
+ }
29
+ if (!matchedSearcheeFiles.length)
30
+ return acc;
31
+ const index = availableFiles.indexOf(matchedSearcheeFiles[0]);
32
+ availableFiles.splice(index, 1);
33
+ const srcFilePath = options.savePath
34
+ ? join(options.savePath, matchedSearcheeFiles[0].path)
35
+ : matchedSearcheeFiles[0].path; // Absolute path
36
+ acc.push([srcFilePath, join(destinationDir, newFile.path)]);
37
+ return acc;
38
+ }, []);
39
+ let alreadyExisted = false;
40
+ let linkedNewFiles = false;
41
+ try {
42
+ logger.verbose({
43
+ label: searchee.label,
44
+ message: `Linking ${getLogString(newMeta)} from ${getLogString(searchee)} to ${destinationDir}`,
45
+ });
46
+ const validPaths = await filterAsync(paths, async ([srcFilePath, destFilePath]) => {
47
+ if (await exists(destFilePath)) {
48
+ logger.verbose({
49
+ label: searchee.label,
50
+ message: `--- Skipping ${srcFilePath} -> ${destFilePath}, already exists`,
51
+ });
52
+ alreadyExisted = true;
53
+ return false;
54
+ }
55
+ if (await exists(srcFilePath))
56
+ return true;
57
+ if (options.ignoreMissing)
58
+ return false;
59
+ throw new Error(`Linking failed, ${srcFilePath} not found.`);
60
+ });
61
+ for (const [srcFilePath, destFilePath] of validPaths) {
62
+ const destFileParentPath = dirname(destFilePath);
63
+ if (await notExists(destFileParentPath)) {
64
+ await mkdir(destFileParentPath, { recursive: true });
65
+ }
66
+ try {
67
+ if (await linkFile(srcFilePath, destFilePath)) {
68
+ linkedNewFiles = true;
69
+ }
70
+ }
71
+ catch (e) {
72
+ logger.error({
73
+ label: searchee.label,
74
+ message: `--- Linking failed, ${srcFilePath} -> ${destFilePath}: ${e.message}`,
75
+ });
76
+ throw e;
77
+ }
78
+ }
79
+ }
80
+ catch (e) {
81
+ return resultOfErr(e);
82
+ }
83
+ return resultOf({ alreadyExisted, linkedNewFiles });
84
+ }
85
+ async function unlinkMetafile(meta, destinationDir, searcheeLabel) {
86
+ const roots = [];
87
+ for (const file of meta.files) {
88
+ const res = getRoot(file);
89
+ if (res.isErr()) {
90
+ const err = res.unwrapErr();
91
+ logger.error({
92
+ label: searcheeLabel,
93
+ message: `Unable to unlink ${getLogString(meta)} in ${destinationDir}: ${err.message}`,
94
+ });
95
+ logger.debug(err);
96
+ return;
97
+ }
98
+ roots.push(join(destinationDir, res.unwrap()));
99
+ }
100
+ const destinationDirIno = (await stat(destinationDir)).ino;
101
+ for (const root of roots) {
102
+ if (await notExists(root))
103
+ continue;
104
+ if (!root.startsWith(destinationDir))
105
+ continue; // assert: root is within destinationDir
106
+ if (resolve(root) === resolve(destinationDir))
107
+ continue; // assert: root is not destinationDir
108
+ if ((await stat(root)).ino === destinationDirIno)
109
+ continue; // assert: root is not destinationDir
110
+ logger.verbose(`Unlinking ${root}`);
111
+ await rm(root, { recursive: true });
112
+ }
113
+ }
114
+ async function getSavePath(searchee, options) {
115
+ if (searchee.path) {
116
+ if (await notExists(searchee.path)) {
117
+ logger.error({
118
+ label: searchee.label,
119
+ message: `Linking failed, ${searchee.path} not found.`,
120
+ });
121
+ return resultOfErr("INVALID_DATA");
122
+ }
123
+ const result = await createSearcheeFromPath(searchee.path);
124
+ if (result.isErr()) {
125
+ return resultOfErr("TORRENT_NOT_FOUND");
126
+ }
127
+ const refreshedSearchee = result.unwrap();
128
+ if (options.onlyCompleted &&
129
+ (searchee.mtimeMs !== refreshedSearchee.mtimeMs ||
130
+ searchee.length !== refreshedSearchee.length)) {
131
+ return resultOfErr("TORRENT_NOT_COMPLETE");
132
+ }
133
+ return resultOf(dirname(searchee.path));
134
+ }
135
+ else if (!searchee.infoHash) {
136
+ for (const file of searchee.files) {
137
+ if (await notExists(file.path)) {
138
+ logger.error(`Linking failed, ${file.path} not found.`);
139
+ return resultOfErr("INVALID_DATA");
140
+ }
141
+ if (options.onlyCompleted) {
142
+ const f = await stat(file.path);
143
+ if (searchee.mtimeMs < f.mtimeMs || file.length !== f.size) {
144
+ return resultOfErr("TORRENT_NOT_COMPLETE");
145
+ }
146
+ }
147
+ }
148
+ return resultOf(undefined);
149
+ }
150
+ const clients = getClients();
151
+ const client = clients.length === 1
152
+ ? clients[0]
153
+ : clients.find((c) => c.clientHost === searchee.clientHost);
154
+ let savePath;
155
+ if (searchee.savePath) {
156
+ if (searchee.label !== Label.INJECT) {
157
+ // This check is too slow for the number of searchees from the inject job
158
+ const refreshedSearchee = (await client.getClientSearchees({
159
+ newSearcheesOnly: true,
160
+ refresh: [searchee.infoHash],
161
+ })).newSearchees.find((s) => s.infoHash === searchee.infoHash);
162
+ if (!refreshedSearchee)
163
+ return resultOfErr("TORRENT_NOT_FOUND");
164
+ Object.assign(searchee, refreshedSearchee);
165
+ }
166
+ if (!(await client.isTorrentComplete(searchee.infoHash)).orElse(false)) {
167
+ return resultOfErr("TORRENT_NOT_COMPLETE");
168
+ }
169
+ savePath = searchee.savePath;
170
+ }
171
+ else {
172
+ const downloadDirResult = await client.getDownloadDir(searchee, { onlyCompleted: options.onlyCompleted });
173
+ if (downloadDirResult.isErr()) {
174
+ return downloadDirResult.mapErr((e) => e === "NOT_FOUND" || e === "UNKNOWN_ERROR"
175
+ ? "TORRENT_NOT_FOUND"
176
+ : e);
177
+ }
178
+ savePath = downloadDirResult.unwrap();
179
+ }
180
+ const rootFolderRes = getRootFolder(searchee.files[0]);
181
+ if (rootFolderRes.isErr()) {
182
+ logger.error({
183
+ label: searchee.label,
184
+ message: `Linking failed, ${rootFolderRes.unwrapErr().message}`,
185
+ });
186
+ return resultOfErr("INVALID_DATA");
187
+ }
188
+ const rootFolder = rootFolderRes.unwrap();
189
+ const sourceRootOrSavePath = searchee.files.length === 1
190
+ ? join(savePath, searchee.files[0].path)
191
+ : rootFolder
192
+ ? join(savePath, rootFolder)
193
+ : savePath;
194
+ if (await notExists(sourceRootOrSavePath)) {
195
+ logger.error({
196
+ label: searchee.label,
197
+ message: `Linking failed, ${sourceRootOrSavePath} not found.`,
198
+ });
199
+ return resultOfErr("INVALID_DATA");
200
+ }
201
+ return resultOf(savePath);
202
+ }
203
+ async function getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker) {
204
+ const { flatLinking, linkType } = getRuntimeConfig();
205
+ if (!client) {
206
+ let srcPath;
207
+ let srcDev;
208
+ try {
209
+ srcPath = !savePath
210
+ ? (await findAsync(searchee.files, (f) => exists(f.path))).path
211
+ : join(savePath, (await findAsync(searchee.files, (f) => exists(join(savePath, f.path)))).path);
212
+ srcDev = (await stat(srcPath)).dev;
213
+ }
214
+ catch (e) {
215
+ logger.debug(e);
216
+ return null;
217
+ }
218
+ let error;
219
+ for (const testClient of getClients().filter((c) => !c.readonly)) {
220
+ const torrentSavePaths = new Set((await testClient.getAllDownloadDirs({
221
+ metas: [],
222
+ onlyCompleted: false,
223
+ })).values());
224
+ if (!torrentSavePaths.size) {
225
+ error = new Error(`No save paths found to test with for ${testClient.label}, add at least one torrent to the client.`);
226
+ continue;
227
+ }
228
+ for (const torrentSavePath of torrentSavePaths) {
229
+ try {
230
+ if (srcDev &&
231
+ (await stat(torrentSavePath)).dev === srcDev) {
232
+ client = testClient;
233
+ break;
234
+ }
235
+ const testPath = join(torrentSavePath, clientDestName);
236
+ await linkFile(srcPath, testPath, linkType === LinkType.REFLINK ||
237
+ linkType === LinkType.REFLINK_OR_COPY
238
+ ? linkType
239
+ : LinkType.HARDLINK);
240
+ await rm(testPath);
241
+ client = testClient;
242
+ break;
243
+ }
244
+ catch (e) {
245
+ error = e;
246
+ }
247
+ }
248
+ if (client)
249
+ break;
250
+ }
251
+ if (!client) {
252
+ logger.debug(error);
253
+ return null;
254
+ }
255
+ }
256
+ let destinationDir;
257
+ let destinationDirFromClient = false;
258
+ const clientSavePathRes = await client.getDownloadDir(newMeta, {
259
+ onlyCompleted: false,
260
+ });
261
+ if (clientSavePathRes.isOk()) {
262
+ destinationDir = clientSavePathRes.unwrap();
263
+ destinationDirFromClient = true;
264
+ }
265
+ else {
266
+ if (clientSavePathRes.unwrapErr() === "INVALID_DATA") {
267
+ return null;
268
+ }
269
+ const linkDir = savePath
270
+ ? await getLinkDir(savePath)
271
+ : await getLinkDirVirtual(searchee);
272
+ if (!linkDir)
273
+ return null;
274
+ destinationDir = flatLinking ? linkDir : join(linkDir, tracker);
275
+ }
276
+ return { client, destinationDir, destinationDirFromClient };
277
+ }
278
+ function logActionResultImpl(result, newMeta, decision, searchee, tracker, infoOrVerbose, warnOrVerbose) {
279
+ const metaLog = getLogString(newMeta, chalk.green.bold);
280
+ const searcheeLog = getLogString(searchee, chalk.magenta.bold);
281
+ const source = `${getSearcheeSource(searchee)} (${searcheeLog})`;
282
+ const foundBy = `Found ${metaLog} on ${chalk.bold(tracker)} by`;
283
+ switch (result) {
284
+ case SaveResult.SAVED:
285
+ infoOrVerbose({
286
+ label: searchee.label,
287
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - saved`,
288
+ });
289
+ break;
290
+ case InjectionResult.SUCCESS:
291
+ infoOrVerbose({
292
+ label: searchee.label,
293
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - injected`,
294
+ });
295
+ break;
296
+ case InjectionResult.ALREADY_EXISTS:
297
+ infoOrVerbose({
298
+ label: searchee.label,
299
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - exists`,
300
+ });
301
+ break;
302
+ case InjectionResult.TORRENT_NOT_COMPLETE:
303
+ warnOrVerbose({
304
+ label: searchee.label,
305
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - source is incomplete, saving...`,
306
+ });
307
+ break;
308
+ case InjectionResult.FAILURE:
309
+ default:
310
+ logger.error({
311
+ label: searchee.label,
312
+ message: `${foundBy} ${chalk.red(decision)} from ${source} - failed to inject, saving...`,
313
+ });
314
+ break;
315
+ }
316
+ }
317
+ async function saveToOutputDir(newMeta, decision, searchee, tracker) {
318
+ const { outputDir } = getRuntimeConfig();
319
+ try {
320
+ const filePath = getTorrentSavePath(newMeta, getMediaType(newMeta), tracker, outputDir, { cached: false });
321
+ if (await exists(filePath)) {
322
+ await utimes(filePath, new Date(), (await stat(filePath)).mtime);
323
+ return;
324
+ }
325
+ await writeFile(filePath, new Uint8Array(newMeta.encode()));
326
+ }
327
+ catch (e) {
328
+ logger.error({
329
+ label: searchee.label,
330
+ message: `Failed to save ${getLogString(newMeta, chalk.green.bold)} on ${chalk.bold(tracker)} to outputDir while processing ${chalk.bold(decision)} with ${getLogString(searchee, chalk.magenta.bold)}: ${e.message}`,
331
+ });
332
+ logger.debug(e);
333
+ }
334
+ }
335
+ export async function performAction(newMeta, decision, searchee, tracker) {
336
+ return withMutex(Mutex.CLIENT_INJECTION, { useQueue: true }, async () => {
337
+ return performActionWithoutMutex(newMeta, decision, searchee, tracker);
338
+ });
339
+ }
340
+ export async function performActionWithoutMutex(newMeta, decision, searchee, tracker, injectClient, options = { onlyCompleted: true }) {
341
+ const { action, linkDirs } = getRuntimeConfig();
342
+ const warnOrVerbose = searchee.label !== Label.INJECT ? logger.warn : logger.verbose;
343
+ const infoOrVerbose = searchee.label !== Label.INJECT ? logger.info : logger.verbose;
344
+ const logActionResult = (r) => logActionResultImpl(r, newMeta, decision, searchee, tracker, infoOrVerbose, warnOrVerbose);
345
+ const saveTorrent = () => saveToOutputDir(newMeta, decision, searchee, tracker);
346
+ if (action === Action.SAVE) {
347
+ const actionResult = SaveResult.SAVED;
348
+ logActionResult(actionResult);
349
+ await saveTorrent();
350
+ return { actionResult };
351
+ }
352
+ let savePath;
353
+ let destinationDir;
354
+ let destinationDirFromClient = false;
355
+ let unlinkOk = false;
356
+ let linkedNewFiles = false;
357
+ try {
358
+ const clients = getClients();
359
+ let client = clients.length === 1
360
+ ? clients[0]
361
+ : clients.find((c) => c.clientHost === searchee.clientHost && !c.readonly);
362
+ const readonlySource = !client && !!searchee.clientHost;
363
+ if (linkDirs.length) {
364
+ const savePathRes = await getSavePath(searchee, options);
365
+ if (savePathRes.isErr()) {
366
+ const result = savePathRes.unwrapErr();
367
+ if (result === "TORRENT_NOT_COMPLETE") {
368
+ const actionResult = InjectionResult.TORRENT_NOT_COMPLETE;
369
+ logActionResult(actionResult);
370
+ await saveTorrent();
371
+ return { client, actionResult, linkedNewFiles };
372
+ }
373
+ const actionResult = InjectionResult.FAILURE;
374
+ logger.error({
375
+ label: searchee.label,
376
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${result}`,
377
+ });
378
+ logActionResult(actionResult);
379
+ await saveTorrent();
380
+ return { actionResult, linkedNewFiles };
381
+ }
382
+ savePath = savePathRes.unwrap();
383
+ const res = await getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker);
384
+ if (res) {
385
+ client = res.client;
386
+ destinationDir = res.destinationDir;
387
+ destinationDirFromClient = res.destinationDirFromClient;
388
+ }
389
+ else {
390
+ client = undefined;
391
+ }
392
+ }
393
+ if (!client) {
394
+ logger.error({
395
+ label: searchee.label,
396
+ message: `Failed to find a torrent client for ${getLogString(searchee)}`,
397
+ });
398
+ const actionResult = InjectionResult.FAILURE;
399
+ logActionResult(actionResult);
400
+ await saveTorrent();
401
+ return { actionResult, linkedNewFiles };
402
+ }
403
+ for (const otherClient of clients) {
404
+ if (otherClient.clientHost === client.clientHost)
405
+ continue;
406
+ const existsRes = await otherClient.isTorrentInClient(newMeta.infoHash);
407
+ if (existsRes.isOk()) {
408
+ if (!existsRes.unwrap())
409
+ continue;
410
+ warnOrVerbose({
411
+ label: searchee.label,
412
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - already exists in ${otherClient.clientHost}`,
413
+ });
414
+ }
415
+ else {
416
+ logger.error({
417
+ label: searchee.label,
418
+ message: `Failed to check if ${getLogString(newMeta)} exists in ${otherClient.clientHost}: ${existsRes.unwrapErr()}`,
419
+ });
420
+ }
421
+ const actionResult = InjectionResult.FAILURE;
422
+ logActionResult(actionResult);
423
+ return { actionResult, linkedNewFiles };
424
+ }
425
+ if (injectClient && injectClient.clientHost !== client.clientHost) {
426
+ warnOrVerbose({
427
+ label: searchee.label,
428
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - existing match is using ${injectClient.clientHost}`,
429
+ });
430
+ const actionResult = InjectionResult.FAILURE;
431
+ logActionResult(actionResult);
432
+ return { actionResult, linkedNewFiles };
433
+ }
434
+ if (linkDirs.length) {
435
+ const res = await linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, { savePath, ignoreMissing: !options.onlyCompleted });
436
+ if (res.isErr()) {
437
+ logger.error({
438
+ label: searchee.label,
439
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
440
+ });
441
+ const actionResult = InjectionResult.FAILURE;
442
+ logActionResult(actionResult);
443
+ await saveTorrent();
444
+ return { actionResult, linkedNewFiles };
445
+ }
446
+ const linkResult = res.unwrap();
447
+ unlinkOk = !linkResult.alreadyExisted;
448
+ linkedNewFiles = linkResult.linkedNewFiles;
449
+ }
450
+ else if (searchee.path) {
451
+ destinationDir = dirname(searchee.path);
452
+ }
453
+ else if (readonlySource) {
454
+ const savePathRes = await getSavePath(searchee, options);
455
+ savePath = savePathRes.orElse(undefined);
456
+ if (!savePath) {
457
+ logger.error({
458
+ label: searchee.label,
459
+ message: `Failed to find a save path for ${getLogString(searchee)}`,
460
+ });
461
+ const actionResult = InjectionResult.FAILURE;
462
+ logActionResult(actionResult);
463
+ await saveTorrent();
464
+ return { actionResult, linkedNewFiles };
465
+ }
466
+ destinationDir = savePath;
467
+ }
468
+ const actionResult = destinationDirFromClient
469
+ ? InjectionResult.ALREADY_EXISTS
470
+ : await client.inject(newMeta, readonlySource
471
+ ? { ...searchee, infoHash: undefined } // treat as data-based
472
+ : searchee, decision, {
473
+ onlyCompleted: options.onlyCompleted,
474
+ destinationDir,
475
+ });
476
+ logActionResult(actionResult);
477
+ if (actionResult === InjectionResult.SUCCESS) {
478
+ // cross-seed may need to process these with the inject job
479
+ if (shouldRecheck(newMeta, decision) || !searchee.infoHash) {
480
+ await saveTorrent();
481
+ }
482
+ }
483
+ else if (actionResult === InjectionResult.ALREADY_EXISTS) {
484
+ if (linkedNewFiles) {
485
+ infoOrVerbose({
486
+ label: client.label,
487
+ message: `Rechecking ${getLogString(newMeta)} as new files were linked from ${getLogString(searchee)}`,
488
+ });
489
+ await client.recheckTorrent(newMeta.infoHash);
490
+ void client.resumeInjection(newMeta, decision, {
491
+ checkOnce: false,
492
+ });
493
+ }
494
+ }
495
+ else {
496
+ await saveTorrent();
497
+ if (unlinkOk && destinationDir) {
498
+ await unlinkMetafile(newMeta, destinationDir, searchee.label);
499
+ linkedNewFiles = false;
500
+ }
501
+ }
502
+ if (actionResult === InjectionResult.FAILURE) {
503
+ return { actionResult, linkedNewFiles };
504
+ }
505
+ return {
506
+ client,
507
+ actionResult,
508
+ destinationDir: destinationDir ?? searchee.savePath,
509
+ linkedNewFiles,
510
+ };
511
+ }
512
+ catch (e) {
513
+ logger.error({
514
+ label: searchee.label,
515
+ message: e,
516
+ });
517
+ const actionResult = InjectionResult.FAILURE;
518
+ logActionResult(actionResult);
519
+ await saveTorrent();
520
+ return { actionResult, linkedNewFiles };
521
+ }
522
+ }
523
+ export async function performActions(searchee, matches) {
524
+ const results = [];
525
+ for (const { tracker, assessment } of matches) {
526
+ const { actionResult } = await performAction(assessment.metafile, assessment.decision, searchee, tracker);
527
+ results.push(actionResult);
528
+ }
529
+ return results;
530
+ }
531
+ async function getLinkDir(pathStr) {
532
+ const { linkDirs, linkType } = getRuntimeConfig();
533
+ const pathStat = await stat(pathStr);
534
+ const pathDev = pathStat.dev; // Windows always returns 0
535
+ if (pathDev) {
536
+ const devs = await mapAsync(linkDirs, async (d) => (await stat(d)).dev);
537
+ if (new Set(devs).size === linkDirs.length) {
538
+ for (const [index, linkDir] of linkDirs.entries()) {
539
+ if (devs[index] === pathDev)
540
+ return linkDir;
541
+ }
542
+ }
543
+ }
544
+ let srcFile = pathStat.isFile()
545
+ ? pathStr
546
+ : pathStat.isDirectory()
547
+ ? await findAFileWithExt(pathStr, ALL_EXTENSIONS)
548
+ : null;
549
+ let tempFile;
550
+ if (!srcFile) {
551
+ tempFile = pathStat.isDirectory()
552
+ ? join(pathStr, linkDirSrcName)
553
+ : join(dirname(pathStr), linkDirSrcName);
554
+ try {
555
+ await writeFile(tempFile, "");
556
+ srcFile = tempFile;
557
+ }
558
+ catch (e) {
559
+ logger.debug(e);
560
+ }
561
+ }
562
+ if (srcFile) {
563
+ for (const linkDir of linkDirs) {
564
+ try {
565
+ const testPath = join(linkDir, linkDirDestName);
566
+ await linkFile(srcFile, testPath, linkType === LinkType.REFLINK ||
567
+ linkType === LinkType.REFLINK_OR_COPY
568
+ ? linkType
569
+ : LinkType.HARDLINK);
570
+ await rm(testPath);
571
+ if (tempFile && (await exists(tempFile)))
572
+ await rm(tempFile);
573
+ return linkDir;
574
+ }
575
+ catch {
576
+ continue;
577
+ }
578
+ }
579
+ }
580
+ if (tempFile && (await exists(tempFile)))
581
+ await rm(tempFile);
582
+ if (linkType !== LinkType.SYMLINK) {
583
+ logger.error(`Cannot find any linkDir from linkDirs on the same drive to ${linkType} ${pathStr}`);
584
+ return null;
585
+ }
586
+ if (linkDirs.length > 1) {
587
+ logger.warn(`Cannot find any linkDir from linkDirs on the same drive, using first linkDir for symlink: ${pathStr}`);
588
+ }
589
+ return linkDirs[0];
590
+ }
591
+ async function getLinkDirVirtual(searchee) {
592
+ const linkDir = await getLinkDir(searchee.files[0].path);
593
+ if (!linkDir)
594
+ return null;
595
+ for (let i = 1; i < searchee.files.length; i++) {
596
+ if ((await getLinkDir(searchee.files[i].path)) !== linkDir) {
597
+ logger.error(`Cannot link files to multiple linkDirs for seasonFromEpisodes aggregation, source episodes are spread across multiple drives.`);
598
+ return null;
599
+ }
600
+ }
601
+ return linkDir;
602
+ }
603
+ async function linkFile(oldPath, newPath, linkType) {
604
+ if (!linkType)
605
+ linkType = getRuntimeConfig().linkType;
606
+ try {
607
+ const ogFileResolvedPath = await unwrapSymlinks(oldPath);
608
+ switch (linkType) {
609
+ case LinkType.HARDLINK:
610
+ await link(ogFileResolvedPath, newPath);
611
+ break;
612
+ case LinkType.SYMLINK:
613
+ // we need to resolve because symlinks are resolved outside
614
+ // the context of cross-seed's working directory
615
+ await symlink(ogFileResolvedPath, resolve(newPath));
616
+ break;
617
+ case LinkType.REFLINK:
618
+ await copyFile(ogFileResolvedPath, newPath, fs.constants.COPYFILE_FICLONE_FORCE);
619
+ break;
620
+ case LinkType.REFLINK_OR_COPY:
621
+ // this will silently fall back to copy if it can't reflink
622
+ await copyFile(ogFileResolvedPath, newPath, fs.constants.COPYFILE_FICLONE);
623
+ break;
624
+ default:
625
+ throw new Error(`Unsupported linkType: ${linkType}`);
626
+ }
627
+ return true;
628
+ }
629
+ catch (e) {
630
+ if (e.code === "EEXIST")
631
+ return false;
632
+ throw e;
633
+ }
634
+ }
635
+ /**
636
+ * Recursively resolves symlinks to the original file. Differs from realpath
637
+ * in that it will not resolve directory symlinks in the middle of the path.
638
+ * @param path
639
+ */
640
+ async function unwrapSymlinks(path) {
641
+ for (let i = 0; i < 16; i++) {
642
+ if (!(await lstat(path)).isSymbolicLink())
643
+ return path;
644
+ path = resolve(dirname(path), await readlink(path));
645
+ }
646
+ throw new Error(`too many levels of symbolic links at ${path}`);
647
+ }
648
+ /**
649
+ * Tests if srcDir supports linkType.
650
+ * @param srcDir The directory to link from
651
+ * @param testSrcName A unique name.cross-seed to create in srcDir if necessary
652
+ * @param testDestName A unique name.cross-seed to create in the linkDir
653
+ * @returns true if the test was successful, false if it failed (or throws)
654
+ */
655
+ export async function testLinking(srcDir, testSrcName, testDestName) {
656
+ const { linkDirs, linkType } = getRuntimeConfig();
657
+ let tempFile;
658
+ try {
659
+ let srcFile = await findAFileWithExt(srcDir, ALL_EXTENSIONS);
660
+ if (!srcFile) {
661
+ const verification = await verifyDir(srcDir, testSrcName, fs.constants.R_OK | fs.constants.W_OK);
662
+ if (!verification.ok) {
663
+ logger.error(`cross-seed is unable to verify linking for ${srcDir} (likely due to incorrect/insufficient volume mounts https://www.cross-seed.org/docs/tutorials/linking#configuring-linkdirs).`);
664
+ return false;
665
+ }
666
+ tempFile = join(srcDir, testSrcName);
667
+ try {
668
+ await writeFile(tempFile, testSrcName);
669
+ srcFile = tempFile;
670
+ }
671
+ catch (e) {
672
+ logger.debug(e);
673
+ logger.error(`Failed to create test file in ${srcDir} for linking test.`);
674
+ return false;
675
+ }
676
+ }
677
+ const linkDir = await getLinkDir(srcDir);
678
+ if (!linkDir)
679
+ throw new Error(`No valid linkDir found for ${srcDir}`);
680
+ const testPath = join(linkDir, testDestName);
681
+ await linkFile(srcFile, testPath);
682
+ await rm(testPath);
683
+ return true;
684
+ }
685
+ catch (e) {
686
+ logger.error(e);
687
+ throw new CrossSeedError(`Failed to create a test ${linkType} from ${srcDir} in any linkDirs: [${formatAsList(linkDirs.map((d) => `"${d}"`), { sort: false, style: "short", type: "unit" })}]. Ensure that ${linkType} is supported between these paths (hardlink/reflink requires same drive, partition, and volume).`);
688
+ }
689
+ finally {
690
+ if (tempFile && (await exists(tempFile)))
691
+ await rm(tempFile);
692
+ }
693
+ }
694
+ //# sourceMappingURL=action.js.map