Skip to content

Commit 9df4a09

Browse files
committed
Split fpb/fput into separate binaries
1 parent ee27f5e commit 9df4a09

File tree

10 files changed

+519
-471
lines changed

10 files changed

+519
-471
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
/.pytest_cache
1111
/.tox
1212
/build
13-
/cli/cli
1413
/dist
1514
/fluffy/static/**/*.hash
1615
/fluffy/static/app.css
1716
/fluffy/static/pygments.css
1817
/settings.py
1918
/tmp
2019
/venv
20+
/fpb
21+
/fput

.goreleaser.yaml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ version: 2
22
snapshot:
33
name_template: '{{.Env.VERSION}}'
44
builds:
5-
- id: cli
6-
main: ./cli
5+
- id: fput
6+
main: ./cli/fput
77
no_unique_dist_dir: true
88
binary: fput-{{.Os}}-{{.Arch}}
9+
- id: fpb
10+
main: ./cli/fpb
11+
no_unique_dist_dir: true
12+
binary: fpb-{{.Os}}-{{.Arch}}
913
archives:
1014
- format: binary
1115
nfpms:
1216
- id: cli
1317
package_name: fluffy
1418
builds:
15-
- cli
19+
- fpb
20+
- fput
1621
homepage: 'https://github.com/chriskuehl/fluffy'
1722
maintainer: 'Chris Kuehl <[email protected]>'
1823
description: 'command-line tools for uploading to fluffy servers'
@@ -22,7 +27,7 @@ nfpms:
2227
- rpm
2328
- archlinux
2429
contents:
25-
- src: /usr/bin/fput-{{.Os}}-{{.Arch}}
30+
- src: /usr/bin/fpb-{{.Os}}-{{.Arch}}
2631
dst: /usr/bin/fpb
2732
type: symlink
2833
- src: /usr/bin/fput-{{.Os}}-{{.Arch}}

Makefile

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ BIN := $(VENV)/bin
33
export FLUFFY_SETTINGS := $(CURDIR)/settings.py
44

55
.PHONY: minimal
6-
minimal: $(VENV) assets settings.py install-hooks
6+
minimal: $(VENV) fpb fput assets settings.py install-hooks
77

8-
cli/cli: cli/main.go go.mod
9-
cd cli && go build -o cli
8+
fpb: cli/fpb/fpb.go cli/internal/cli/cli.go go.mod
9+
go build ./cli/fpb
10+
11+
fput: cli/fput/fput.go cli/internal/cli/cli.go go.mod
12+
go build ./cli/fput
1013

1114
.PHONY: release-cli
1215
release-cli: export GORELEASER_CURRENT_TAG ?= 0.0.0
@@ -15,12 +18,12 @@ release-cli:
1518
go run github.com/goreleaser/goreleaser/v2@latest release --clean --snapshot --verbose
1619
rm -v dist/*.txt dist/*.yaml dist/*.json
1720

18-
$(VENV): setup.py requirements.txt requirements-dev.txt cli/cli
21+
$(VENV): setup.py requirements.txt requirements-dev.txt
1922
rm -rf $@
2023
virtualenv -ppython3.11 $@
2124
$@/bin/pip install -r requirements.txt -r requirements-dev.txt -e .
22-
ln -fs ../../cli/cli $@/bin/fput
23-
ln -fs ../../cli/cli $@/bin/fpb
25+
ln -fs ../../fput $@/bin/fput
26+
ln -fs ../../fpb $@/bin/fpb
2427

2528
fluffy/static/app.css: $(VENV) $(wildcard fluffy/static/scss/*.scss)
2629
$(BIN)/pysassc fluffy/static/scss/app.scss $@

cli/fpb

Lines changed: 0 additions & 1 deletion
This file was deleted.

cli/fpb/fpb.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"mime/multipart"
10+
"net/http"
11+
"os"
12+
"regexp"
13+
"runtime/debug"
14+
"strings"
15+
16+
"github.com/spf13/cobra"
17+
18+
"github.com/chriskuehl/fluffy/cli/internal/cli"
19+
)
20+
21+
// This will be set by the linker for release builds.
22+
var version = "(dev)"
23+
24+
func regexHighlightFragment(regex *regexp.Regexp, content *bytes.Buffer) (string, error) {
25+
scanner := bufio.NewScanner(strings.NewReader(content.String()))
26+
matches := []int{}
27+
for i := 0; scanner.Scan(); {
28+
line := scanner.Text()
29+
if regex.MatchString(line) {
30+
matches = append(matches, i)
31+
}
32+
i++
33+
}
34+
if err := scanner.Err(); err != nil {
35+
return "", fmt.Errorf("scanning: %w", err)
36+
}
37+
38+
// Squash consecutive matches.
39+
groups := []string{}
40+
for i := 0; i < len(matches); i++ {
41+
start := matches[i]
42+
end := start
43+
for ; len(matches) > i+1 && matches[i+1] == end+1; i++ {
44+
end++
45+
}
46+
if start == end {
47+
groups = append(groups, fmt.Sprintf("L%d", start+1))
48+
} else {
49+
groups = append(groups, fmt.Sprintf("L%d-%d", start+1, end+1))
50+
}
51+
}
52+
53+
return strings.Join(groups, ","), nil
54+
}
55+
56+
// Variant of bufio.ScanLines which includes the newline character in the token.
57+
//
58+
// This is desired so that we don't erroneously insert a newline at the end of a final line which
59+
// isn't present in the input. bufio.ScanLines has no way to differentiate whether or not the final
60+
// line has a newline or not.
61+
func ScanLinesWithEOL(data []byte, atEOF bool) (advance int, token []byte, err error) {
62+
if atEOF && len(data) == 0 {
63+
return 0, nil, nil
64+
}
65+
if i := bytes.IndexByte(data, '\n'); i >= 0 {
66+
// We have a full newline-terminated line.
67+
return i + 1, data[0 : i+1], nil
68+
}
69+
// If we're at EOF, we have a final, non-terminated line. Return it.
70+
if atEOF {
71+
return len(data), data, nil
72+
}
73+
// Request more data.
74+
return 0, nil, nil
75+
}
76+
77+
var command = &cobra.Command{
78+
Use: "fpb [file]",
79+
Long: `Paste text to fluffy.
80+
81+
Example usage:
82+
83+
Paste a file:
84+
fpb some-file.txt
85+
86+
Pipe the output of a command:
87+
some-command | fpb
88+
89+
Specify a language to highlight text with:
90+
fpb -l python some_file.py
91+
(Default is to auto-detect the language. You can use "rendered-markdown" for Markdown.)
92+
93+
` + cli.Description,
94+
Args: cobra.MaximumNArgs(1),
95+
SilenceUsage: true,
96+
97+
RunE: cli.WrapWithAuth(func(cmd *cobra.Command, args []string, creds *cli.Credentials) error {
98+
server, _ := cmd.Flags().GetString("server")
99+
tee, _ := cmd.Flags().GetBool("tee")
100+
language, _ := cmd.Flags().GetString("language")
101+
directLink, _ := cmd.Flags().GetBool("direct-link")
102+
103+
path := "-"
104+
if len(args) > 0 {
105+
path = args[0]
106+
}
107+
108+
body := &bytes.Buffer{}
109+
writer := multipart.NewWriter(body)
110+
writer.WriteField("language", language)
111+
112+
content := &bytes.Buffer{}
113+
if path == "-" {
114+
scanner := bufio.NewScanner(os.Stdin)
115+
scanner.Split(ScanLinesWithEOL)
116+
for scanner.Scan() {
117+
line := scanner.Text()
118+
if _, err := content.WriteString(line); err != nil {
119+
return fmt.Errorf("writing to buffer: %w", err)
120+
}
121+
if tee {
122+
fmt.Print(line)
123+
}
124+
}
125+
if err := scanner.Err(); err != nil {
126+
return fmt.Errorf("reading from stdin: %w", err)
127+
}
128+
} else {
129+
file, err := os.Open(path)
130+
if err != nil {
131+
return fmt.Errorf("opening file: %w", err)
132+
}
133+
defer file.Close()
134+
if _, err := io.Copy(content, file); err != nil {
135+
return fmt.Errorf("copying file: %w", err)
136+
}
137+
}
138+
writer.WriteField("text", content.String())
139+
140+
if err := writer.Close(); err != nil {
141+
return fmt.Errorf("closing writer: %w", err)
142+
}
143+
144+
req, err := http.NewRequest("POST", server+"/paste?json", body)
145+
if err != nil {
146+
return fmt.Errorf("creating request: %w", err)
147+
}
148+
req.Header.Set("Content-Type", writer.FormDataContentType())
149+
if creds != nil {
150+
req.SetBasicAuth(creds.Username, creds.Password)
151+
}
152+
q := req.URL.Query()
153+
q.Add("language", language)
154+
req.URL.RawQuery = q.Encode()
155+
156+
resp, err := http.DefaultClient.Do(req)
157+
if err != nil {
158+
return fmt.Errorf("making request: %w", err)
159+
}
160+
defer resp.Body.Close()
161+
162+
if resp.StatusCode != http.StatusOK {
163+
fmt.Fprintf(os.Stderr, "Unexpected status code: %d\n", resp.StatusCode)
164+
fmt.Fprintf(os.Stderr, "Error:\n")
165+
err := fmt.Errorf("unexpected status code: %d", resp.StatusCode)
166+
if _, copyErr := io.Copy(os.Stderr, resp.Body); copyErr != nil {
167+
return fmt.Errorf("copying error: %w for %w", copyErr, err)
168+
}
169+
return err
170+
}
171+
172+
var result struct {
173+
Redirect string `json:"redirect"`
174+
UploadedFiles struct {
175+
Paste struct {
176+
Raw string `json:"raw"`
177+
} `json:"paste"`
178+
} `json:"uploaded_files"`
179+
}
180+
181+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
182+
return fmt.Errorf("decoding response: %w", err)
183+
}
184+
185+
location := result.Redirect
186+
if directLink {
187+
location = result.UploadedFiles.Paste.Raw
188+
}
189+
190+
if regex.r != nil {
191+
highlight, err := regexHighlightFragment(regex.r, content)
192+
if err != nil {
193+
return fmt.Errorf("highlighting: %w", err)
194+
}
195+
location += "#" + highlight
196+
}
197+
198+
fmt.Println(cli.Bold(location))
199+
return nil
200+
}),
201+
}
202+
203+
type regexpValue struct {
204+
r *regexp.Regexp
205+
}
206+
207+
func (v *regexpValue) String() string {
208+
if v.r == nil {
209+
return ""
210+
}
211+
return v.r.String()
212+
}
213+
214+
func (v *regexpValue) Set(s string) error {
215+
r, err := regexp.Compile(s)
216+
if err != nil {
217+
return err
218+
}
219+
v.r = r
220+
return nil
221+
}
222+
223+
func (v *regexpValue) Type() string {
224+
return "regex"
225+
}
226+
227+
var regex = regexpValue{}
228+
229+
func init() {
230+
buildInfo, ok := debug.ReadBuildInfo()
231+
if !ok {
232+
fmt.Fprintf(os.Stderr, "Failed to read build info\n")
233+
os.Exit(1)
234+
}
235+
command.Version = fmt.Sprintf("%s/%s", version, buildInfo.GoVersion)
236+
237+
settings, err := cli.GetSettings()
238+
if err != nil {
239+
panic(fmt.Errorf("getting settings: %w", err))
240+
}
241+
242+
cli.AddCommonOpts(command, settings)
243+
244+
command.Flags().StringP("language", "l", "autodetect", "language for syntax highlighting")
245+
command.Flags().VarP(&regex, "regex", "r", "regex of lines to highlight")
246+
command.Flags().Bool("tee", false, "stream the stdin to stdout before creating the paste")
247+
}
248+
249+
func main() {
250+
if err := command.Execute(); err != nil {
251+
os.Exit(1)
252+
}
253+
}
File renamed without changes.

cli/fput

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)