Skip to content

Commit 34034c4

Browse files
authored
Merge pull request #24 from rtmelsov/develop
Develop
2 parents 23da04b + 61b256c commit 34034c4

File tree

22 files changed

+724
-286
lines changed

22 files changed

+724
-286
lines changed

api/proto/file/v1/file.proto

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
syntax = "proto3";
32

43
package file.v1;
@@ -55,11 +54,15 @@ message UploadResponse {
5554
string sha256 = 3; // контрольная сумма принятого
5655
}
5756

57+
message FileEof {
58+
string sha256_hex = 1;
59+
}
5860

5961
message DownloadFileResponse {
6062
oneof payload {
6163
FileInfo info = 1; // ПЕРВОЕ сообщение — мета-инфа
6264
FileChunk chunk = 2; // Далее — куски файла
65+
FileEof eof = 3;
6366
}
6467
}
6568

db/queries/auth.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ WHERE id = $1;
3333
SELECT id, user_id, filename, path, size_bytes, created_at
3434
FROM files
3535
WHERE id = $1 AND user_id = $2;
36+

docs/screenshot.png

3.54 MB
Loading

gen/go/proto/file/v1/file.pb.go

Lines changed: 142 additions & 74 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/akclient/download_file.go

Lines changed: 71 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package akclient
22

33
import (
44
"crypto/sha256"
5+
"encoding/hex"
56
"fmt"
67
"io"
7-
"log"
88
"os"
99
"path/filepath"
10+
"strings"
1011

1112
"google.golang.org/grpc"
1213

@@ -22,53 +23,35 @@ import (
2223
"google.golang.org/grpc/status"
2324
)
2425

25-
func safeBase(name string) string {
26-
// убираем директории и опасные символы
27-
base := filepath.Base(name)
28-
runes := make([]rune, 0, len(base))
29-
for _, r := range base {
30-
switch {
31-
case r >= 'a' && r <= 'z',
32-
r >= 'A' && r <= 'Z',
33-
r >= '0' && r <= '9',
34-
r == '.', r == '-', r == '_', r == ' ':
35-
runes = append(runes, r)
36-
}
37-
}
38-
if len(runes) == 0 {
39-
return "file"
40-
}
41-
// защитим длину
42-
if len(runes) > 128 {
43-
runes = runes[:128]
44-
}
45-
return string(runes)
46-
}
4726
func DownloadFile(fileID string, prog chan<- models.Prog) {
4827
defer close(prog)
28+
4929
ctx, err := middleware.AddAuthData()
5030
if err != nil {
5131
prog <- models.Prog{Err: err}
5232
return
5333
}
54-
outDir := helpers.DownloadFilesDir
55-
conn, err := grpc.NewClient(helpers.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
34+
35+
outDir, err := helpers.GetDownloadsDir()
5636
if err != nil {
5737
prog <- models.Prog{Err: err}
58-
log.Fatalf("dial %s: %v", helpers.Addr, err)
38+
return
39+
}
40+
41+
conn, err := grpc.DialContext(ctx, helpers.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
42+
if err != nil {
43+
prog <- models.Prog{Err: err}
44+
return
5945
}
6046
defer conn.Close()
6147

62-
// 2) gRPC-клиент
6348
c := filev1.NewFileServiceClient(conn)
64-
6549
stream, err := c.DownloadFile(ctx, &filev1.DownloadFileRequest{Fileid: fileID})
6650
if err != nil {
6751
prog <- models.Prog{Err: err}
6852
return
6953
}
7054

71-
// 1) ждём FileInfo
7255
first, err := stream.Recv()
7356
if err != nil {
7457
prog <- models.Prog{Err: status.Errorf(codes.Internal, "read: %v", err)}
@@ -80,12 +63,9 @@ func DownloadFile(fileID string, prog chan<- models.Prog) {
8063
return
8164
}
8265

83-
filename := safeBase(info.GetFilename())
84-
if filename == "" {
85-
filename = "file"
86-
}
66+
filename := helpers.NextAvailableName(outDir, info.GetFilename())
67+
total := info.GetSize()
8768

88-
// 2) готовим пути/директории
8969
if err := os.MkdirAll(outDir, 0o755); err != nil {
9070
prog <- models.Prog{Err: fmt.Errorf("mkdir: %w", err)}
9171
return
@@ -98,78 +78,95 @@ func DownloadFile(fileID string, prog chan<- models.Prog) {
9878
prog <- models.Prog{Err: fmt.Errorf("create: %w", err)}
9979
return
10080
}
81+
var retErr error
10182
defer func() {
102-
out.Close()
103-
if err != nil {
104-
_ = os.Remove(tmpPath)
83+
_ = out.Close()
84+
if retErr != nil {
85+
_ = os.Remove(tmpPath) // удаляем именно .part при ошибке
10586
}
10687
}()
10788

108-
stat, err := out.Stat()
109-
if err != nil {
110-
prog <- models.Prog{Err: err}
111-
return
112-
}
113-
total := stat.Size()
114-
11589
h := sha256.New()
11690
var written int64
91+
var eofHex string
11792

11893
for {
11994
if ctx.Err() != nil {
120-
prog <- models.Prog{Err: ctx.Err()}
95+
retErr = ctx.Err()
96+
prog <- models.Prog{Err: retErr}
12197
return
12298
}
99+
123100
msg, rerr := stream.Recv()
124101
if rerr == io.EOF {
125102
break
126103
}
127104
if rerr != nil {
128-
// красиво покажем gRPC статус
129105
if st, ok := status.FromError(rerr); ok {
130-
prog <- models.Prog{Err: fmt.Errorf("recv: %s", st.Message())}
131-
return
106+
retErr = fmt.Errorf("recv: %s", st.Message())
107+
} else {
108+
retErr = fmt.Errorf("recv: %w", rerr)
132109
}
133-
prog <- models.Prog{Err: fmt.Errorf("recv: %w", rerr)}
134-
return
135-
}
136-
137-
ch := msg.GetChunk()
138-
if ch == nil {
139-
prog <- models.Prog{Err: errors.New("unexpected message: want FileChunk")}
110+
prog <- models.Prog{Err: retErr}
140111
return
141112
}
142113

143-
n, werr := out.Write(ch.Content)
144-
if werr != nil {
145-
prog <- models.Prog{Err: fmt.Errorf("write: %w", werr)}
146-
return
114+
if e := msg.GetEof(); e != nil {
115+
eofHex = strings.ToLower(strings.TrimSpace(e.GetSha256Hex()))
116+
continue
147117
}
148-
written += int64(n)
149-
select {
150-
case prog <- models.Prog{Done: written, Total: total}:
151-
default: // не блокируем UI, если буфер заполнен
118+
if ch := msg.GetChunk(); ch != nil {
119+
n, werr := out.Write(ch.Content)
120+
if werr != nil {
121+
retErr = fmt.Errorf("write: %w", werr)
122+
prog <- models.Prog{Err: retErr}
123+
return
124+
}
125+
written += int64(n)
126+
select {
127+
case prog <- models.Prog{Done: written, Total: total}:
128+
default:
129+
}
130+
_, _ = h.Write(ch.Content)
131+
continue
152132
}
153-
_, _ = h.Write(ch.Content)
133+
134+
retErr = errors.New("unexpected message: want chunk or eof")
135+
prog <- models.Prog{Err: retErr}
136+
return
154137
}
155138

156-
// 4) fsync → close → rename
157-
if err := out.Sync(); err != nil {
158-
prog <- models.Prog{Err: fmt.Errorf("sync: %w", err)}
139+
gotHex := strings.ToLower(hex.EncodeToString(h.Sum(nil)))
140+
141+
if eofHex == "" {
142+
retErr = errors.New("missing EOF sha256 from server")
143+
prog <- models.Prog{Err: retErr}
159144
return
160145
}
161-
if err := out.Close(); err != nil {
162-
prog <- models.Prog{Err: fmt.Errorf("close: %w", err)}
146+
if !strings.EqualFold(gotHex, eofHex) {
147+
retErr = fmt.Errorf("sha256 mismatch: got %s, want %s", gotHex, eofHex)
148+
prog <- models.Prog{Err: retErr}
163149
return
164150
}
165-
if err := os.Rename(tmpPath, finalPath); err != nil {
166-
prog <- models.Prog{Err: fmt.Errorf("rename: %w", err)}
151+
if total > 0 && written != total {
152+
retErr = fmt.Errorf("size mismatch: got %d, want %d", written, total)
153+
prog <- models.Prog{Err: retErr}
167154
return
168155
}
169156

170-
// 5) сверим размер (если сервер прислал size)
171-
if info.Size > 0 && written != info.Size {
172-
prog <- models.Prog{Err: fmt.Errorf("size mismatch: got %d, want %d", written, info.Size)}
157+
if err := out.Sync(); err != nil {
158+
retErr = fmt.Errorf("sync: %w", err)
159+
prog <- models.Prog{Err: retErr}
160+
return
161+
}
162+
if err := out.Close(); err != nil {
163+
retErr = fmt.Errorf("close: %w", err)
164+
prog <- models.Prog{Err: retErr}
165+
return
166+
}
167+
if err := os.Rename(tmpPath, finalPath); err != nil {
168+
retErr = fmt.Errorf("rename: %w", err)
169+
prog <- models.Prog{Err: retErr}
173170
return
174171
}
175172
}

internal/fileserver/get_file.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/hex"
66
"io"
77
"os"
8-
"path/filepath"
98

109
"github.com/google/uuid"
1110

@@ -58,7 +57,7 @@ func (s *Service) DownloadFile(DownloadFileRequest *filev1.DownloadFileRequest,
5857
err = stream.Send(&filev1.DownloadFileResponse{
5958
Payload: &filev1.DownloadFileResponse_Info{
6059
Info: &filev1.FileInfo{
61-
Filename: filepath.Base(path),
60+
Filename: u.Filename,
6261
Size: stat.Size(),
6362
},
6463
},
@@ -93,19 +92,26 @@ func (s *Service) DownloadFile(DownloadFileRequest *filev1.DownloadFileRequest,
9392
}
9493
}
9594
if rerr == io.EOF {
96-
break
95+
if err := stream.Send(&filev1.DownloadFileResponse{
96+
Payload: &filev1.DownloadFileResponse_Eof{
97+
Eof: &filev1.FileEof{Sha256Hex: hex.EncodeToString(h.Sum(nil))},
98+
},
99+
}); err != nil {
100+
return err
101+
}
102+
103+
// 3) закрываем отправку и получаем ответ
104+
log.Info("download done",
105+
"file", u.Path,
106+
"bytes", u.SizeBytes,
107+
"sha256", hex.EncodeToString(h.Sum(nil)),
108+
)
109+
return nil
97110
}
98111
if rerr != nil {
99112
return status.Errorf(codes.Internal, "read: %v", rerr)
100113
}
101114
}
102115

103-
// 3) закрываем отправку и получаем ответ
104-
log.Info("download done",
105-
"file", u.Path,
106-
"bytes", u.SizeBytes,
107-
"sha256", hex.EncodeToString(h.Sum(nil)),
108-
)
109116
return nil
110-
111117
}

0 commit comments

Comments
 (0)