@@ -2,11 +2,12 @@ package akclient
22
33import (
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- }
4726func 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}
0 commit comments