effect-libreoffice 1.0.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.
- package/.github/workflows/ci.yml +40 -0
- package/.vscode/settings.json +3 -0
- package/Dockerfile +12 -0
- package/README.md +82 -0
- package/biome.json +43 -0
- package/compose.yml +19 -0
- package/docker/ubuntu.Dockerfile +3 -0
- package/examples/cloud-run/compose.yml +5 -0
- package/examples/cloud-run/uno.Dockerfile +9 -0
- package/fixtures/test.txt +7 -0
- package/package.json +50 -0
- package/src/index.test.ts +126 -0
- package/src/index.ts +180 -0
- package/src/misc/benchmark.ts +116 -0
- package/src/misc/scratchpad.ts +26 -0
- package/src/shared.ts +30 -0
- package/src/ubuntu-docker.test.ts +61 -0
- package/src/uno/schema-utils.test.ts +58 -0
- package/src/uno/schema-utils.ts +44 -0
- package/src/uno/uno-response.ts +102 -0
- package/src/uno/uno.test.ts +192 -0
- package/src/uno/uno.ts +226 -0
- package/src/uno/xml-parser.ts +16 -0
- package/tsconfig.json +25 -0
- package/tsdown.config.ts +9 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install pnpm
|
|
17
|
+
uses: pnpm/action-setup@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v6
|
|
21
|
+
with:
|
|
22
|
+
node-version: 24
|
|
23
|
+
cache: "pnpm"
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: pnpm install
|
|
27
|
+
|
|
28
|
+
- name: Install LibreOffice
|
|
29
|
+
run: |
|
|
30
|
+
sudo apt-get update
|
|
31
|
+
sudo apt-get install -y --no-install-recommends libreoffice-writer
|
|
32
|
+
|
|
33
|
+
- name: Lint
|
|
34
|
+
run: pnpm biome ci .
|
|
35
|
+
|
|
36
|
+
- name: Type check
|
|
37
|
+
run: pnpm type-check
|
|
38
|
+
|
|
39
|
+
- name: Test
|
|
40
|
+
run: pnpm test
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FROM alpine:latest
|
|
2
|
+
|
|
3
|
+
# install fonts https://wiki.alpinelinux.org/wiki/Fonts
|
|
4
|
+
RUN apk add --no-cache font-terminus font-inconsolata font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra
|
|
5
|
+
|
|
6
|
+
# install libreoffice + dependencies
|
|
7
|
+
RUN apk add --no-cache libreoffice-writer python3 py3-pip openjdk11-jre-headless
|
|
8
|
+
|
|
9
|
+
# install unoserver via pip
|
|
10
|
+
RUN pip install unoserver --break-system-packages
|
|
11
|
+
|
|
12
|
+
CMD ["unoserver", "--interface", "0.0.0.0"]
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# effect-libreoffice
|
|
2
|
+
|
|
3
|
+
A Effect-based wrapper for LibreOffice, providing multiple strategies for document conversion.
|
|
4
|
+
|
|
5
|
+
## Implementations
|
|
6
|
+
|
|
7
|
+
This library offers two distinct implementations for interacting with LibreOffice:
|
|
8
|
+
|
|
9
|
+
1. **LibreOfficeCmd (Default)**: Uses the `soffice` command-line tool directly.
|
|
10
|
+
2. **UnoClient (Uno)**: Connects to a running `unoserver` instance.
|
|
11
|
+
|
|
12
|
+
### Comparison
|
|
13
|
+
|
|
14
|
+
| Feature | LibreOfficeCmd (Default) | UnoClient (Uno) |
|
|
15
|
+
| :-------------- | :--------------------------------------- | :----------------------------------------- |
|
|
16
|
+
| **Method** | Spawns a new process for each conversion | Connects to a long-running server |
|
|
17
|
+
| **Performance** | Slower (~440ms/file) | Fast (~60ms/file) |
|
|
18
|
+
| **Setup** | Requires LibreOffice installed locally | Requires `unoserver` |
|
|
19
|
+
| **Best For** | CLI tools, low volume, simple setup | Servers, high volume, performance critical |
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Default Implementation (CLI)
|
|
24
|
+
|
|
25
|
+
Best for quick scripts or when you don't want to manage a separate server.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { LibreOffice } from "effect-libreoffice";
|
|
29
|
+
import { NodeContext } from "@effect/platform-node";
|
|
30
|
+
import { Effect } from "effect";
|
|
31
|
+
|
|
32
|
+
const program = Effect.gen(function* () {
|
|
33
|
+
const libre = yield* LibreOffice;
|
|
34
|
+
yield* libre.convertLocalFile("input.docx", "output.pdf");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Provide the Default layer (which uses LibreOfficeCmd) and NodeContext
|
|
38
|
+
program.pipe(
|
|
39
|
+
Effect.provide(LibreOffice.Default),
|
|
40
|
+
Effect.provide(NodeContext.layer),
|
|
41
|
+
Effect.runPromise
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Uno Implementation (Remote)
|
|
46
|
+
|
|
47
|
+
Best for server environments. You need a running `unoserver`.
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# compose.yml
|
|
51
|
+
services:
|
|
52
|
+
unoserver:
|
|
53
|
+
image: libreofficedocker/libreoffice-unoserver:3.22
|
|
54
|
+
ports:
|
|
55
|
+
- "2003:2003"
|
|
56
|
+
volumes:
|
|
57
|
+
- /tmp:/tmp # some shared directory where files can be written and read
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { LibreOffice, UnoClient, UnoServer } from "effect-libreoffice";
|
|
62
|
+
import { NodeContext, NodeHttpClient } from "@effect/platform-node";
|
|
63
|
+
import { Effect, Layer } from "effect";
|
|
64
|
+
|
|
65
|
+
const program = Effect.gen(function* () {
|
|
66
|
+
const libre = yield* LibreOffice;
|
|
67
|
+
yield* libre.convertLocalFile("input.docx", "output.pdf");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const UnoLayer = LibreOffice.Uno.pipe(
|
|
71
|
+
Layer.provide(UnoClient.Default),
|
|
72
|
+
Layer.provide(UnoServer.Remote) // Defaults to localhost:2003
|
|
73
|
+
// Layer.provide(UnoServer.remoteWithURL("http://unoserver:2003/RPC2"))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
program.pipe(
|
|
77
|
+
Effect.provide(UnoLayer),
|
|
78
|
+
Effect.provide(NodeHttpClient.layer),
|
|
79
|
+
Effect.provide(NodeContext.layer),
|
|
80
|
+
Effect.runPromise
|
|
81
|
+
);
|
|
82
|
+
```
|
package/biome.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"indentStyle": "space"
|
|
14
|
+
},
|
|
15
|
+
"linter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"rules": {
|
|
18
|
+
"recommended": true,
|
|
19
|
+
"suspicious": {
|
|
20
|
+
"noShadowRestrictedNames": "off"
|
|
21
|
+
},
|
|
22
|
+
"correctness": {
|
|
23
|
+
"noUnusedVariables": {
|
|
24
|
+
"level": "warn",
|
|
25
|
+
"fix": "none"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"javascript": {
|
|
31
|
+
"formatter": {
|
|
32
|
+
"quoteStyle": "double"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"assist": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"actions": {
|
|
38
|
+
"source": {
|
|
39
|
+
"organizeImports": "on"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/compose.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
services:
|
|
2
|
+
# unoserver:
|
|
3
|
+
# image: libreofficedocker/libreoffice-unoserver:3.22
|
|
4
|
+
# ports:
|
|
5
|
+
# - "2004:2004" # uno rest api
|
|
6
|
+
# - "2003:2003" # uno api
|
|
7
|
+
unoserver:
|
|
8
|
+
build: .
|
|
9
|
+
ports:
|
|
10
|
+
- "2003:2003" # uno api
|
|
11
|
+
volumes:
|
|
12
|
+
- /tmp/test-convert:/tmp/test-convert
|
|
13
|
+
user: "1000:1000"
|
|
14
|
+
environment:
|
|
15
|
+
- HOME=/tmp
|
|
16
|
+
develop:
|
|
17
|
+
watch:
|
|
18
|
+
- action: rebuild
|
|
19
|
+
path: ./Dockerfile
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "effect-libreoffice",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A effect based LibreOffice converter library",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"author": "Filip Weiss <me@fiws.net>",
|
|
9
|
+
"license": "ISC",
|
|
10
|
+
"homepage": "https://github.com/fiws/effect-libreoffice",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/fiws/effect-libreoffice.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/fiws/effect-libreoffice/issues"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.mjs",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"fast-xml-parser": "^5.3.3"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@effect/platform": "^0.93.3",
|
|
28
|
+
"effect": "^3.19.6"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "2.3.10",
|
|
32
|
+
"@effect/language-service": "^0.62.4",
|
|
33
|
+
"@effect/platform": "^0.94.0",
|
|
34
|
+
"@effect/platform-node": "^0.104.0",
|
|
35
|
+
"@effect/vitest": "^0.27.0",
|
|
36
|
+
"effect": "^3.19.13",
|
|
37
|
+
"testcontainers": "^11.10.0",
|
|
38
|
+
"tinybench": "^6.0.0",
|
|
39
|
+
"tsdown": "^0.18.2",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.16"
|
|
43
|
+
},
|
|
44
|
+
"types": "./dist/index.d.mts",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"type-check": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { FileSystem, Path } from "@effect/platform";
|
|
2
|
+
import { NodeContext } from "@effect/platform-node";
|
|
3
|
+
import { assert, expect, it } from "@effect/vitest";
|
|
4
|
+
import { Effect, Layer, Predicate } from "effect";
|
|
5
|
+
import { LibreOffice } from "./index";
|
|
6
|
+
|
|
7
|
+
const TestLive = Layer.provideMerge(LibreOffice.Default, NodeContext.layer);
|
|
8
|
+
|
|
9
|
+
it.layer(TestLive)("Libreoffice (Default)", (it) => {
|
|
10
|
+
it.scoped(
|
|
11
|
+
"should convert a file",
|
|
12
|
+
Effect.fn(function* () {
|
|
13
|
+
const fs = yield* FileSystem.FileSystem;
|
|
14
|
+
const path = yield* Path.Path;
|
|
15
|
+
const libre = yield* LibreOffice;
|
|
16
|
+
|
|
17
|
+
const tempDir = yield* fs.makeTempDirectory();
|
|
18
|
+
const sourceFile = path.join(tempDir, "test.txt");
|
|
19
|
+
const targetFile = path.join(tempDir, "test.out.pdf");
|
|
20
|
+
|
|
21
|
+
yield* fs.writeFileString(sourceFile, "Hello PDF");
|
|
22
|
+
yield* libre.convertLocalFile(sourceFile, targetFile);
|
|
23
|
+
|
|
24
|
+
const targetContent = yield* fs.readFile(targetFile);
|
|
25
|
+
|
|
26
|
+
const header = new TextDecoder().decode(targetContent.slice(0, 4));
|
|
27
|
+
expect(header).toBe("%PDF");
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
it.effect(
|
|
32
|
+
"should fails with source file not found",
|
|
33
|
+
Effect.fn(function* () {
|
|
34
|
+
const libre = yield* LibreOffice;
|
|
35
|
+
const result = yield* libre
|
|
36
|
+
.convertLocalFile("./fixtures/test-not-found.txt", "test.out.pdf")
|
|
37
|
+
.pipe(Effect.flip);
|
|
38
|
+
|
|
39
|
+
expect(result._tag).toBe("LibreOfficeError");
|
|
40
|
+
|
|
41
|
+
// assertion for type narrowing
|
|
42
|
+
assert(
|
|
43
|
+
Predicate.isTagged(result, "LibreOfficeError"),
|
|
44
|
+
"result is not LibreOfficeError",
|
|
45
|
+
);
|
|
46
|
+
expect(result.reason).toBe("InputFileNotFound");
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
it.effect(
|
|
51
|
+
"Should work with 2 conversions in parallel",
|
|
52
|
+
Effect.fn(function* () {
|
|
53
|
+
const libre = yield* LibreOffice;
|
|
54
|
+
const fs = yield* FileSystem.FileSystem;
|
|
55
|
+
const path = yield* Path.Path;
|
|
56
|
+
|
|
57
|
+
const tempDir = yield* fs.makeTempDirectory();
|
|
58
|
+
const sourceFile = path.join(tempDir, "test.txt");
|
|
59
|
+
const targetFile = path.join(tempDir, "test.out.pdf");
|
|
60
|
+
|
|
61
|
+
yield* fs.writeFileString(sourceFile, "Hello PDF");
|
|
62
|
+
|
|
63
|
+
// will internaly use a semaphore to limit parallel conversions to 1
|
|
64
|
+
yield* Effect.all(
|
|
65
|
+
[
|
|
66
|
+
libre.convertLocalFile(sourceFile, targetFile),
|
|
67
|
+
libre.convertLocalFile(sourceFile, targetFile),
|
|
68
|
+
],
|
|
69
|
+
{ concurrency: "unbounded" },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const targetContent = yield* fs.readFile(targetFile);
|
|
73
|
+
|
|
74
|
+
const header = new TextDecoder().decode(targetContent.slice(0, 4));
|
|
75
|
+
expect(header).toBe("%PDF");
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it.effect(
|
|
80
|
+
"should fail with invalid output extension",
|
|
81
|
+
Effect.fn(function* () {
|
|
82
|
+
const libre = yield* LibreOffice;
|
|
83
|
+
const fs = yield* FileSystem.FileSystem;
|
|
84
|
+
const path = yield* Path.Path;
|
|
85
|
+
const tempDir = yield* fs.makeTempDirectory();
|
|
86
|
+
const sourceFile = path.join(tempDir, "test.txt");
|
|
87
|
+
const targetFile = path.join(tempDir, "test.invalidext");
|
|
88
|
+
|
|
89
|
+
yield* fs.writeFileString(sourceFile, "Hello PDF");
|
|
90
|
+
|
|
91
|
+
const result = yield* libre
|
|
92
|
+
.convertLocalFile(sourceFile, targetFile)
|
|
93
|
+
.pipe(Effect.flip);
|
|
94
|
+
|
|
95
|
+
assert(
|
|
96
|
+
Predicate.isTagged(result, "LibreOfficeError"),
|
|
97
|
+
"result is not LibreOfficeError",
|
|
98
|
+
);
|
|
99
|
+
expect(result.reason).toBe("BadOutputExtension");
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it.effect(
|
|
104
|
+
"should fail with output as directory",
|
|
105
|
+
Effect.fn(function* () {
|
|
106
|
+
const libre = yield* LibreOffice;
|
|
107
|
+
const fs = yield* FileSystem.FileSystem;
|
|
108
|
+
const path = yield* Path.Path;
|
|
109
|
+
const tempDir = yield* fs.makeTempDirectory();
|
|
110
|
+
const sourceFile = path.join(tempDir, "test.txt");
|
|
111
|
+
const targetFile = tempDir;
|
|
112
|
+
|
|
113
|
+
yield* fs.writeFileString(sourceFile, "Hello PDF");
|
|
114
|
+
|
|
115
|
+
const result = yield* libre
|
|
116
|
+
.convertLocalFile(sourceFile, targetFile)
|
|
117
|
+
.pipe(Effect.flip);
|
|
118
|
+
|
|
119
|
+
assert(
|
|
120
|
+
Predicate.isTagged(result, "LibreOfficeError"),
|
|
121
|
+
"result is not LibreOfficeError",
|
|
122
|
+
);
|
|
123
|
+
expect(result.reason).toBe("BadOutputExtension");
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Command, FileSystem, Path } from "@effect/platform";
|
|
2
|
+
import { Context, Effect, flow, Layer, Match, Stream, String } from "effect";
|
|
3
|
+
import { LibreOfficeError, type OutputPath } from "./shared";
|
|
4
|
+
import { UnoClient, UnoError } from "./uno/uno";
|
|
5
|
+
|
|
6
|
+
const runString = <E, R>(
|
|
7
|
+
stream: Stream.Stream<Uint8Array, E, R>,
|
|
8
|
+
): Effect.Effect<string, E, R> =>
|
|
9
|
+
stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat));
|
|
10
|
+
|
|
11
|
+
export class LibreOfficeCmd extends Context.Reference<LibreOfficeCmd>()(
|
|
12
|
+
"libre-convert-effect/index/LibreOfficeCmd",
|
|
13
|
+
{ defaultValue: () => ["soffice", "--headless"] },
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
export class LibreOffice extends Effect.Service<LibreOffice>()(
|
|
17
|
+
"libre-convert-effect/index/LibreOffice",
|
|
18
|
+
{
|
|
19
|
+
// #region Default
|
|
20
|
+
scoped: Effect.gen(function* () {
|
|
21
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22
|
+
const path = yield* Path.Path;
|
|
23
|
+
const sem = yield* Effect.makeSemaphore(1);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
/**
|
|
27
|
+
* Converts a file to a different format.
|
|
28
|
+
*
|
|
29
|
+
* ### Example
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const program = Effect.gen(function* () {
|
|
33
|
+
* const libre = yield* LibreOffice;
|
|
34
|
+
* yield* libre.convertLocalFile("/path/to/input.docx", "/path/to/output.pdf");
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
convertLocalFile: Effect.fn(function* (
|
|
39
|
+
input: string,
|
|
40
|
+
output: OutputPath,
|
|
41
|
+
) {
|
|
42
|
+
const [cmd, ...args] = yield* LibreOfficeCmd;
|
|
43
|
+
|
|
44
|
+
const parsedInput = path.parse(input);
|
|
45
|
+
const parsedOutput = path.parse(output);
|
|
46
|
+
|
|
47
|
+
// to preserve compatiblity with unoserver we have to check if the output is a directory
|
|
48
|
+
if (
|
|
49
|
+
yield* fs.stat(output).pipe(
|
|
50
|
+
Effect.map((stat) => stat.type === "Directory"),
|
|
51
|
+
Effect.catchAll(() => Effect.succeed(false)),
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
return yield* Effect.fail(
|
|
55
|
+
new LibreOfficeError({
|
|
56
|
+
reason: "BadOutputExtension",
|
|
57
|
+
message: "Output path is a directory",
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// we need a temporary directory to ensure conversions do not conflict
|
|
63
|
+
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
64
|
+
prefix: "effect-libreoffice-",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// libreoffice does not do well with parallel conversions. It works if we provide
|
|
68
|
+
// a new "UserInstallation" for each conversion but this slows down execution by about 8x
|
|
69
|
+
// so we use a semaphore to limit parallel conversions to 1
|
|
70
|
+
yield* sem.withPermits(1)(
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const process = yield* Command.make(
|
|
73
|
+
cmd,
|
|
74
|
+
...args,
|
|
75
|
+
"--convert-to",
|
|
76
|
+
parsedOutput.ext.slice(1),
|
|
77
|
+
"--outdir",
|
|
78
|
+
tempDir,
|
|
79
|
+
input,
|
|
80
|
+
).pipe(Command.start);
|
|
81
|
+
// We need to wait for the process to exit to get the exit code
|
|
82
|
+
// and capture stderr in parallel to avoid missing output
|
|
83
|
+
const [exitCode, result] = yield* Effect.all(
|
|
84
|
+
[process.exitCode, runString(process.stderr)],
|
|
85
|
+
{ concurrency: "unbounded" },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Check for specific errors in stderr first, regardless of exit code
|
|
89
|
+
yield* Match.value(String.trim(result)).pipe(
|
|
90
|
+
Match.when(
|
|
91
|
+
String.includes("Error: source file could not be loaded"),
|
|
92
|
+
() =>
|
|
93
|
+
new LibreOfficeError({
|
|
94
|
+
reason: "InputFileNotFound",
|
|
95
|
+
message: result,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
Match.when(
|
|
99
|
+
String.includes("Error: no export filter"),
|
|
100
|
+
() =>
|
|
101
|
+
new LibreOfficeError({
|
|
102
|
+
reason: "BadOutputExtension",
|
|
103
|
+
message: result,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
Match.when(
|
|
107
|
+
String.includes("Permission denied"),
|
|
108
|
+
() =>
|
|
109
|
+
new LibreOfficeError({
|
|
110
|
+
reason: "PermissionDenied",
|
|
111
|
+
message: result,
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
Match.when(
|
|
115
|
+
String.includes("Error: "),
|
|
116
|
+
() =>
|
|
117
|
+
new LibreOfficeError({
|
|
118
|
+
reason: "Unknown",
|
|
119
|
+
message: result,
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
Match.orElse(() => Effect.void),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (exitCode !== 0) {
|
|
126
|
+
return yield* new LibreOfficeError({
|
|
127
|
+
reason: "Unknown",
|
|
128
|
+
message:
|
|
129
|
+
result || `Process failed with exit code ${exitCode}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// using the libreoffice cli we can not specify the output file name
|
|
134
|
+
// it will be the input file name with the extension changed to the output format
|
|
135
|
+
const libreOutputPath = path.join(
|
|
136
|
+
tempDir,
|
|
137
|
+
String.concat(parsedInput.name, parsedOutput.ext),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// so we rename the file to the expected output path
|
|
141
|
+
yield* fs.copyFile(libreOutputPath, output);
|
|
142
|
+
|
|
143
|
+
// (temp directory is cleaned up by finalizer from makeTempDirectoryScoped)
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}, Effect.scoped),
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
// #endregion
|
|
150
|
+
},
|
|
151
|
+
) {
|
|
152
|
+
// #region Uno
|
|
153
|
+
/**
|
|
154
|
+
* The Uno layer uses a unoserver to convert files. It is much more
|
|
155
|
+
* performant than the cli but requires a unoserver to be running.
|
|
156
|
+
*/
|
|
157
|
+
static Uno = Layer.scoped(
|
|
158
|
+
LibreOffice,
|
|
159
|
+
Effect.gen(function* () {
|
|
160
|
+
const client = yield* UnoClient;
|
|
161
|
+
|
|
162
|
+
return LibreOffice.make({
|
|
163
|
+
convertLocalFile: flow(
|
|
164
|
+
client.convert,
|
|
165
|
+
Effect.as(undefined),
|
|
166
|
+
Effect.mapError((err) =>
|
|
167
|
+
err instanceof UnoError
|
|
168
|
+
? new LibreOfficeError(err)
|
|
169
|
+
: new LibreOfficeError({
|
|
170
|
+
reason: "Unknown",
|
|
171
|
+
message: `Failed to convert file: ${err}`,
|
|
172
|
+
cause: err,
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
});
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
// #endregion
|
|
180
|
+
}
|