├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api ├── request.go └── server.go ├── compose.yml ├── depot └── deport.go ├── go.mod ├── go.sum ├── unoconvert └── unoconvert.go └── unoserver-rest-api.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | tags: v*.*.* 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: '>=1.19.0' 24 | 25 | - name: Build binary 26 | run: | 27 | make 28 | make build VERSION=${{ github.event.release.tag_name }} 29 | 30 | - name: Upload releases 31 | uses: softprops/action-gh-release@v1 32 | with: 33 | files: build/* 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/osx,go 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=osx,go 5 | 6 | ### Go ### 7 | # If you prefer the allow list template instead of the deny list, see community template: 8 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 9 | # 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, built with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | # vendor/ 25 | 26 | # Go workspace file 27 | go.work 28 | 29 | ### Go Patch ### 30 | /vendor/ 31 | /Godeps/ 32 | 33 | ### OSX ### 34 | # General 35 | .DS_Store 36 | .AppleDouble 37 | .LSOverride 38 | 39 | # Icon must end with two \r 40 | Icon 41 | 42 | 43 | # Thumbnails 44 | ._* 45 | 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | # End of https://www.toptal.com/developers/gitignore/api/osx,go 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | COPY build/unoserver-rest-api-${TARGETOS}-${TARGETARCH} /unoserver-rest-api 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=local 2 | DOCKER_REGISTRY=libreoffice-docker 3 | DOCKER_NAME=libreoffice-unoserver-rest-api 4 | DOCKER_TAG=nightly 5 | DOCKER_IMAGE=${DOCKER_REGISTRY}/${DOCKER_NAME}:${DOCKER_TAG} 6 | 7 | OUTPUT := build 8 | 9 | UNAME_S := $(shell uname -s) 10 | ifeq ($(UNAME_S),Darwin) 11 | SHA_CMD := shasum -a 256 12 | else 13 | SHA_CMD := sha256sum 14 | endif 15 | 16 | install: 17 | @go mod tidy 18 | 19 | run: 20 | @go run unoserver-rest-api.go 21 | 22 | build: 23 | $(call go-build,linux,amd64) 24 | $(call go-build,linux,arm64) 25 | 26 | build-darwin: 27 | $(call go-build,darwin,amd64) 28 | $(call go-build,darwin,arm64) 29 | 30 | build-docker: build 31 | DOCKER_BUILDKIT=1 docker build --rm -f "Dockerfile" -t ${DOCKER_IMAGE} "." 32 | 33 | run-docker: 34 | docker run -it --rm -p "2003:2003" ${DOCKER_IMAGE} 35 | 36 | clean: 37 | @rm -rf $(OUTPUT); true 38 | 39 | # define function 40 | define go-build 41 | @echo "- Building for $(1)-$(2)..." 42 | @echo 43 | @CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build -ldflags="-s -w -X main.Version=${VERSION}" -o $(OUTPUT)/unoserver-rest-api-$(1)-$(2) unoserver-rest-api.go 44 | @upx $(OUTPUT)/unoserver-rest-api-$(1)-$(2) 45 | @cd $(OUTPUT) && $(SHA_CMD) unoserver-rest-api-$(1)-$(2) > unoserver-rest-api-$(1)-$(2).sha256 46 | endef 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unoserver-rest-api 2 | 3 | The simple REST API for unoserver 4 | 5 | > **Warning** 6 | > 7 | > It is important to know that the REST API layer DOES NOT provide any type of security whatsoever. 8 | > It is NOT RECOMMENDED to expose this container image to the internet. 9 | 10 | ## Usage 11 | 12 | Unoserver needs to be installed, see [Installation](https://github.com/unoconv/unoserver#installation) guide. 13 | 14 | ```sh 15 | NAME: 16 | unoserver-rest-api - The simple REST API for unoserver and unoconvert 17 | 18 | GLOBAL OPTIONS: 19 | --addr value The addr used by the unoserver api server (default: "0.0.0.0:2004") 20 | --unoserver-addr value The unoserver addr used by the unoconvert (default: "127.0.0.1:2002") [$UNOSERVER_ADDR] 21 | --unoconvert-bin value Set the unoconvert executable path. (default: "unoconvert") [$UNOCONVERT_BIN] 22 | --unoconvert-timeout value Set the unoconvert run timeout (default: 0s) [$UNOCONVERT_TIMEOUT] 23 | --help, -h show help 24 | --version, -v print the version 25 | ``` 26 | 27 | ### Using with Docker 28 | 29 | The [libreofficedocker/libreoffice-unoserver](https://github.com/libreofficedocker/libreoffice-unoserver) already have `unoserver-rest-api` included within the Docker image. 30 | 31 | ## API 32 | 33 | There is only one POST `/request` API. 34 | 35 | **Default payload** 36 | 37 | ```sh 38 | curl -s -v \ 39 | --request POST \ 40 | --url http://127.0.0.1:2004/request \ 41 | --header 'Content-Type: multipart/form-data' \ 42 | --form "file=@/path/to/your/file.xlsx" \ 43 | --form 'convert-to=pdf' \ 44 | --output 'file.pdf' 45 | ``` 46 | 47 | - `file`: Type of `File`, required 48 | - `convert-to`: Type of `String`, required 49 | 50 | **Advance payload** 51 | 52 | ```sh 53 | curl -s -v \ 54 | --request POST \ 55 | --url http://127.0.0.1:2004/request \ 56 | --header 'Content-Type: multipart/form-data' \ 57 | --form "file=@/path/to/your/file.xlsx" \ 58 | --form 'convert-to=pdf' \ 59 | --form 'opts[]=--landscape' \ 60 | --output 'file.pdf' 61 | ``` 62 | 63 | - `file`: Type of `File`, required 64 | - `convert-to`: Type of `String`, required 65 | - `opts`: Type of `String[]` 66 | 67 | ## License 68 | 69 | Licensed under [Apache-2.0 license](LICENSE). 70 | -------------------------------------------------------------------------------- /api/request.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "mime/multipart" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/libreofficedocker/unoserver-rest-api/depot" 13 | "github.com/libreofficedocker/unoserver-rest-api/unoconvert" 14 | ) 15 | 16 | type RequestForm struct { 17 | Name string `form:"name"` 18 | Options []string `form:"opts[]"` 19 | ConvertTo string `form:"convert-to" binding:"required"` 20 | File *multipart.FileHeader `form:"file" binding:"required"` 21 | } 22 | 23 | func RequestHandler(c *gin.Context) { 24 | var err error 25 | var form RequestForm 26 | 27 | if err := c.ShouldBind(&form); err != nil { 28 | c.String(http.StatusBadRequest, err.Error()) 29 | return 30 | } 31 | 32 | var tempFilename = "*" 33 | 34 | if form.Name == "" { 35 | form.Name = form.File.Filename 36 | } 37 | 38 | tempFilename += "-" + form.Name 39 | 40 | inFile, err := os.CreateTemp(depot.WorkDir, tempFilename) 41 | if err != nil { 42 | log.Println("Create temp file failed", err) 43 | c.String(http.StatusInternalServerError, "unknown error") 44 | return 45 | } 46 | filePath := inFile.Name() 47 | defer func() { 48 | err := os.Remove(filePath) 49 | if err != nil { 50 | log.Println("Delege temp file failed", err) 51 | } 52 | }() 53 | 54 | // Save file to working directory 55 | err = c.SaveUploadedFile(form.File, filePath) 56 | if err != nil { 57 | log.Println("Convert failed", err) 58 | c.String(http.StatusInternalServerError, "unknown error") 59 | return 60 | } 61 | 62 | // Prepare output file path 63 | outFile, err := os.CreateTemp(depot.WorkDir, tempFilename+"."+form.ConvertTo) 64 | if err != nil { 65 | log.Println("Create temp file failed", err) 66 | c.String(http.StatusInternalServerError, "unknown error") 67 | return 68 | } 69 | defer func() { 70 | err := os.Remove(outFile.Name()) 71 | if err != nil { 72 | log.Println("Delege temp file failed", err) 73 | } 74 | }() 75 | 76 | // Run unoconvert command with options 77 | // If context timeout is 0s run without timeout 78 | if unoconvert.ContextTimeout == 0 { 79 | err = unoconvert.Run(inFile.Name(), outFile.Name(), form.Options...) 80 | } else { 81 | err = unoconvert.RunContext(context.Background(), inFile.Name(), outFile.Name(), form.Options...) 82 | } 83 | 84 | log.Printf("Processing: %s %s %s", inFile.Name(), outFile.Name(), form.Options) 85 | 86 | if err != nil { 87 | log.Printf("unoconvert error: %s", err) 88 | c.String(http.StatusInternalServerError, fmt.Sprintf("unoconvert error: %s", err)) 89 | return 90 | } 91 | 92 | // Send the converted file to body stream 93 | c.File(outFile.Name()) 94 | } 95 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "log" 5 | "syscall" 6 | 7 | "github.com/fvbock/endless" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func init() { 12 | gin.SetMode(gin.ReleaseMode) 13 | gin.DisableConsoleColor() 14 | } 15 | 16 | func ListenAndServe(addr string) { 17 | router := gin.Default() 18 | router.SetTrustedProxies(nil) 19 | 20 | // Routes 21 | router.POST("/request", RequestHandler) 22 | 23 | if addr == "" { 24 | addr = ":2004" 25 | } 26 | 27 | pm := endless.NewServer(addr, router) 28 | pm.BeforeBegin = func(add string) { 29 | log.Printf("Server is running at %s", addr) 30 | log.Printf("Server is running pid is %d", syscall.Getpid()) 31 | } 32 | 33 | pm.ListenAndServe() 34 | } 35 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | libreoffice: 3 | build: 4 | context: . 5 | restart: always 6 | ports: 7 | - 2003:2003 8 | -------------------------------------------------------------------------------- /depot/deport.go: -------------------------------------------------------------------------------- 1 | package depot 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var WorkDir string = "work" 9 | var WorkDirPattern string = "unodata-*" 10 | 11 | func MkdirTemp() { 12 | d, _ := os.MkdirTemp("", WorkDirPattern) 13 | WorkDir = d 14 | log.Printf("Server working directory '%s'", d) 15 | } 16 | 17 | func CleanTemp() { 18 | log.Printf("Removing directory '%s'", WorkDir) 19 | os.RemoveAll(WorkDir) 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/libreofficedocker/unoserver-rest-api 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/urfave/cli v1.22.10 9 | ) 10 | 11 | require ( 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 13 | github.com/gin-contrib/sse v0.1.0 // indirect 14 | github.com/go-playground/locales v0.14.0 // indirect 15 | github.com/go-playground/universal-translator v0.18.0 // indirect 16 | github.com/go-playground/validator/v10 v10.10.0 // indirect 17 | github.com/goccy/go-json v0.9.7 // indirect 18 | github.com/google/go-cmp v0.5.8 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/mattn/go-isatty v0.0.14 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 25 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 26 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 27 | github.com/ugorji/go/codec v1.2.7 // indirect 28 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 29 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 30 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 31 | golang.org/x/text v0.3.7 // indirect 32 | google.golang.org/protobuf v1.28.1 // indirect 33 | gopkg.in/yaml.v2 v2.4.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= 9 | github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= 10 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 11 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 12 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 13 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 14 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 15 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 16 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 17 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 18 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 19 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 20 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 21 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 22 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 23 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 24 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 25 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 27 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 30 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 31 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 32 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 33 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 34 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 35 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 39 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 40 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 41 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 42 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 47 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 48 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 49 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 50 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 54 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 55 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 56 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 57 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 59 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 62 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 65 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 66 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 67 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 68 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 69 | github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= 70 | github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 71 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 72 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 73 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 74 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 75 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 81 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 86 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 87 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 88 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 90 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 91 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 95 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 96 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 97 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 99 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 102 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /unoconvert/unoconvert.go: -------------------------------------------------------------------------------- 1 | package unoconvert 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os/exec" 7 | "time" 8 | ) 9 | 10 | var ( 11 | DefaultContextTimeout = 0 * time.Minute 12 | ) 13 | 14 | var ( 15 | ContextTimeout = DefaultContextTimeout 16 | ) 17 | 18 | var unoconvert = &Unoconvert{ 19 | Interface: "127.0.0.1", 20 | Port: "2002", 21 | Executable: "unoconvert", 22 | } 23 | 24 | func SetExecutable(executable string) { 25 | unoconvert.SetExecutable(executable) 26 | } 27 | 28 | func SetInterface(interf string) { 29 | unoconvert.SetInterface(interf) 30 | } 31 | 32 | func SetPort(port string) { 33 | unoconvert.SetPort(port) 34 | } 35 | 36 | func SetContextTimeout(timeout time.Duration) { 37 | unoconvert.SetContextTimeout(timeout) 38 | } 39 | 40 | func Run(infile string, outfile string, opts ...string) error { 41 | return unoconvert.Run(infile, outfile, opts...) 42 | } 43 | 44 | func RunContext(ctx context.Context, infile string, outfile string, opts ...string) error { 45 | return unoconvert.RunContext(ctx, infile, outfile, opts...) 46 | } 47 | 48 | type Unoconvert struct { 49 | Interface string 50 | Port string 51 | Executable string 52 | } 53 | 54 | func (u *Unoconvert) SetExecutable(executable string) { 55 | u.Executable = executable 56 | } 57 | 58 | func (u *Unoconvert) SetInterface(interf string) { 59 | u.Interface = interf 60 | } 61 | 62 | func (u *Unoconvert) SetPort(port string) { 63 | u.Port = port 64 | } 65 | 66 | func (u *Unoconvert) SetContextTimeout(timeout time.Duration) { 67 | ContextTimeout = timeout 68 | } 69 | 70 | func (u *Unoconvert) Run(infile string, outfile string, opts ...string) error { 71 | var args = []string{} 72 | 73 | connections := []string{ 74 | fmt.Sprintf("--interface=%s", u.Interface), 75 | fmt.Sprintf("--port=%s", u.Port), 76 | } 77 | 78 | files := []string{infile, outfile} 79 | 80 | args = append(connections, files...) 81 | args = append(args, opts...) 82 | 83 | cmd := exec.Command(u.Executable, args...) 84 | 85 | return cmd.Run() 86 | } 87 | 88 | func (u *Unoconvert) RunContext(ctx context.Context, infile string, outfile string, opts ...string) error { 89 | ctx, cancel := context.WithTimeout(ctx, ContextTimeout) 90 | defer cancel() 91 | 92 | var args = []string{} 93 | 94 | connections := []string{ 95 | fmt.Sprintf("--interface=%s", u.Interface), 96 | fmt.Sprintf("--port=%s", u.Port), 97 | } 98 | 99 | files := []string{infile, outfile} 100 | 101 | args = append(connections, files...) 102 | args = append(args, opts...) 103 | 104 | cmd := exec.CommandContext(ctx, u.Executable, args...) 105 | return cmd.Run() 106 | } 107 | -------------------------------------------------------------------------------- /unoserver-rest-api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "github.com/libreofficedocker/unoserver-rest-api/api" 10 | "github.com/libreofficedocker/unoserver-rest-api/depot" 11 | "github.com/libreofficedocker/unoserver-rest-api/unoconvert" 12 | "github.com/urfave/cli" 13 | ) 14 | 15 | var Version = "unstable" 16 | 17 | func init() { 18 | log.SetPrefix("unoserver-rest-api ") 19 | } 20 | 21 | func main() { 22 | app := cli.NewApp() 23 | app.Name = "unoserver-rest-api" 24 | app.Version = Version 25 | app.Usage = "The simple REST API for unoserver and unoconvert" 26 | app.Flags = []cli.Flag{ 27 | cli.StringFlag{ 28 | Name: "addr", 29 | Value: "0.0.0.0:2004", 30 | Usage: "The addr used by the unoserver api server", 31 | }, 32 | cli.StringFlag{ 33 | Name: "unoserver-addr", 34 | Value: "127.0.0.1:2002", 35 | Usage: "The unoserver addr used by the unoconvert", 36 | EnvVar: "UNOSERVER_ADDR", 37 | }, 38 | cli.StringFlag{ 39 | Name: "unoconvert-bin", 40 | Value: "unoconvert", 41 | Usage: "Set the unoconvert executable path.", 42 | EnvVar: "UNOCONVERT_BIN", 43 | }, 44 | cli.DurationFlag{ 45 | Name: "unoconvert-timeout", 46 | Value: 0 * time.Minute, 47 | Usage: "Set the unoconvert run timeout", 48 | EnvVar: "UNOCONVERT_TIMEOUT", 49 | }, 50 | } 51 | app.Authors = []cli.Author{ 52 | { 53 | Name: "libreoffice-docker", 54 | Email: "https://github.com/libreofficedocker/unoserver-rest-api", 55 | }, 56 | } 57 | app.Action = mainAction 58 | 59 | if err := app.Run(os.Args); err != nil { 60 | os.Exit(1) 61 | } 62 | } 63 | 64 | func mainAction(c *cli.Context) { 65 | // Create temporary working directory 66 | depot.MkdirTemp() 67 | 68 | // Cleanup temporary working directory after finished 69 | defer depot.CleanTemp() 70 | 71 | // Configure unoconvert options 72 | unoAddr := c.String("unoserver-addr") 73 | host, port, _ := net.SplitHostPort(unoAddr) 74 | unoconvert.SetInterface(host) 75 | unoconvert.SetPort(port) 76 | unoconvert.SetExecutable(c.String("unoconvert-bin")) 77 | unoconvert.SetContextTimeout(c.Duration("unoconvert-timeout")) 78 | 79 | // Start the API server 80 | addr := c.String("addr") 81 | api.ListenAndServe(addr) 82 | } 83 | --------------------------------------------------------------------------------