Skip to content

Commit e826f6e

Browse files
authored
Merge pull request #56 from slonopotamus/upload-fixes
Fixed to object upload
2 parents 0fe6a31 + dd28f0f commit e826f6e

File tree

6 files changed

+195
-150
lines changed

6 files changed

+195
-150
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ See https://github.com/bozaro/git-lfs-java/blob/master/gitlfs-server/src/test/ja
5454

5555
## Changes
5656

57-
Version 0.14.0 (Unreleased)
57+
Version 0.14.0
58+
59+
* Add LFS object verification server code
60+
* Fix broken handling of already existing LFS object
5861

5962
Version 0.13.3
6063

gitlfs-client/src/main/java/ru/bozaro/gitlfs/client/Client.java

Lines changed: 99 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,56 @@ public ObjectRes getMeta(@NotNull final String hash) throws IOException {
9090
), Operation.Download);
9191
}
9292

93+
/**
94+
* Upload object with specified hash and size.
95+
*
96+
* @param streamProvider Object stream provider.
97+
* @param hash Object hash.
98+
* @param size Object size.
99+
* @return Return true is object is uploaded successfully and false if object is already uploaded.
100+
* @throws IOException On some errors.
101+
*/
102+
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final String hash, final long size) throws IOException {
103+
return putObject(streamProvider, new Meta(hash, size));
104+
}
105+
106+
/**
107+
* Upload object with specified hash and size.
108+
*
109+
* @param streamProvider Object stream provider.
110+
* @param meta Object metadata.
111+
* @return Return true is object is uploaded successfully and false if object is already uploaded.
112+
* @throws IOException On some errors.
113+
*/
114+
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final Meta meta) throws IOException {
115+
return doWork(auth -> {
116+
final ObjectRes links = doRequest(auth, new MetaPost(meta), AuthHelper.join(auth.getHref(), PATH_OBJECTS));
117+
return links != null && putObject(streamProvider, meta, links);
118+
}, Operation.Upload);
119+
}
120+
121+
protected <T> T doWork(@NotNull Work<T> work, @NotNull Operation operation) throws IOException {
122+
Link auth = authProvider.getAuth(operation);
123+
int authCount = 0;
124+
while (true) {
125+
try {
126+
return work.exec(auth);
127+
} catch (UnauthorizedException | ForbiddenException e) {
128+
if (authCount >= MAX_AUTH_COUNT) {
129+
throw e;
130+
}
131+
authCount++;
132+
// Get new authentication data.
133+
authProvider.invalidateAuth(operation, auth);
134+
final Link newAuth = authProvider.getAuth(operation);
135+
if (newAuth.getHeader().equals(auth.getHeader()) && newAuth.getHref().equals(auth.getHref())) {
136+
throw e;
137+
}
138+
auth = newAuth;
139+
}
140+
}
141+
}
142+
93143
/**
94144
* Get metadata for object by hash.
95145
*
@@ -191,104 +241,6 @@ public boolean putObject(@NotNull final StreamProvider streamProvider) throws IO
191241
return putObject(streamProvider, generateMeta(streamProvider));
192242
}
193243

194-
/**
195-
* Generate object metadata.
196-
*
197-
* @param streamProvider Object stream provider.
198-
* @return Return object metadata.
199-
* @throws IOException On some errors.
200-
*/
201-
public static Meta generateMeta(@NotNull final StreamProvider streamProvider) throws IOException {
202-
final MessageDigest digest = sha256();
203-
final byte[] buffer = new byte[0x10000];
204-
long size = 0;
205-
try (InputStream stream = streamProvider.getStream()) {
206-
while (true) {
207-
int read = stream.read(buffer);
208-
if (read <= 0) break;
209-
digest.update(buffer, 0, read);
210-
size += read;
211-
}
212-
}
213-
return new Meta(new String(Hex.encodeHex(digest.digest())), size);
214-
}
215-
216-
/**
217-
* Upload object with specified hash and size.
218-
*
219-
* @param streamProvider Object stream provider.
220-
* @param hash Object hash.
221-
* @param size Object size.
222-
* @return Return true is object is uploaded successfully and false if object is already uploaded.
223-
* @throws IOException On some errors.
224-
*/
225-
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final String hash, final long size) throws IOException {
226-
return putObject(streamProvider, new Meta(hash, size));
227-
}
228-
229-
/**
230-
* Upload object with specified hash and size.
231-
*
232-
* @param streamProvider Object stream provider.
233-
* @param meta Object metadata.
234-
* @return Return true is object is uploaded successfully and false if object is already uploaded.
235-
* @throws IOException On some errors.
236-
*/
237-
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final Meta meta) throws IOException {
238-
return doWork(auth -> {
239-
final ObjectRes links = doRequest(auth, new MetaPost(meta), AuthHelper.join(auth.getHref(), PATH_OBJECTS));
240-
return links != null && putObject(streamProvider, meta, links);
241-
}, Operation.Upload);
242-
}
243-
244-
/**
245-
* Upload object by metadata.
246-
*
247-
* @param links Object links.
248-
* @param streamProvider Object stream provider.
249-
* @param meta Object metadata.
250-
* @return Return true is object is uploaded successfully and false if object is already uploaded.
251-
* @throws IOException On some errors.
252-
*/
253-
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final Meta meta, @NotNull final Links links) throws IOException {
254-
if (links.getLinks().containsKey(LinkType.Download)) {
255-
return false;
256-
}
257-
final Link uploadLink = links.getLinks().get(LinkType.Upload);
258-
if (uploadLink == null) {
259-
throw new IOException("Upload link not found");
260-
}
261-
doRequest(uploadLink, new ObjectPut(streamProvider, meta.getSize()), uploadLink.getHref());
262-
263-
final Link verifyLink = links.getLinks().get(LinkType.Verify);
264-
if (verifyLink != null) {
265-
doRequest(verifyLink, new ObjectVerify(meta), verifyLink.getHref());
266-
}
267-
return true;
268-
}
269-
270-
protected <T> T doWork(@NotNull Work<T> work, @NotNull Operation operation) throws IOException {
271-
Link auth = authProvider.getAuth(operation);
272-
int authCount = 0;
273-
while (true) {
274-
try {
275-
return work.exec(auth);
276-
} catch (UnauthorizedException | ForbiddenException e) {
277-
if (authCount >= MAX_AUTH_COUNT) {
278-
throw e;
279-
}
280-
authCount++;
281-
// Get new authentication data.
282-
authProvider.invalidateAuth(operation, auth);
283-
final Link newAuth = authProvider.getAuth(operation);
284-
if (newAuth.getHeader().equals(auth.getHeader()) && newAuth.getHref().equals(auth.getHref())) {
285-
throw e;
286-
}
287-
auth = newAuth;
288-
}
289-
}
290-
}
291-
292244
public <R> R doRequest(@Nullable Link link, @NotNull Request<R> task, @NotNull URI url) throws IOException {
293245
int redirectCount = 0;
294246
int retryCount = 0;
@@ -342,12 +294,49 @@ public <R> R doRequest(@Nullable Link link, @NotNull Request<R> task, @NotNull U
342294
}
343295
}
344296

345-
protected void addHeaders(@NotNull HttpUriRequest req, @Nullable Link link) {
346-
if (link != null) {
347-
for (Map.Entry<String, String> entry : link.getHeader().entrySet()) {
348-
req.setHeader(entry.getKey(), entry.getValue());
297+
/**
298+
* Generate object metadata.
299+
*
300+
* @param streamProvider Object stream provider.
301+
* @return Return object metadata.
302+
* @throws IOException On some errors.
303+
*/
304+
public static Meta generateMeta(@NotNull final StreamProvider streamProvider) throws IOException {
305+
final MessageDigest digest = sha256();
306+
final byte[] buffer = new byte[0x10000];
307+
long size = 0;
308+
try (InputStream stream = streamProvider.getStream()) {
309+
while (true) {
310+
int read = stream.read(buffer);
311+
if (read <= 0) break;
312+
digest.update(buffer, 0, read);
313+
size += read;
349314
}
350315
}
316+
return new Meta(new String(Hex.encodeHex(digest.digest())), size);
317+
}
318+
319+
/**
320+
* Upload object by metadata.
321+
*
322+
* @param links Object links.
323+
* @param streamProvider Object stream provider.
324+
* @param meta Object metadata.
325+
* @return Return true is object is uploaded successfully and false if object is already uploaded.
326+
* @throws IOException On some errors.
327+
*/
328+
public boolean putObject(@NotNull final StreamProvider streamProvider, @NotNull final Meta meta, @NotNull final Links links) throws IOException {
329+
final Link uploadLink = links.getLinks().get(LinkType.Upload);
330+
if (uploadLink == null)
331+
return false;
332+
333+
doRequest(uploadLink, new ObjectPut(streamProvider, meta.getSize()), uploadLink.getHref());
334+
335+
final Link verifyLink = links.getLinks().get(LinkType.Verify);
336+
if (verifyLink != null)
337+
doRequest(verifyLink, new ObjectVerify(meta), verifyLink.getHref());
338+
339+
return true;
351340
}
352341

353342
protected static MessageDigest sha256() {
@@ -358,6 +347,14 @@ protected static MessageDigest sha256() {
358347
}
359348
}
360349

350+
protected void addHeaders(@NotNull HttpUriRequest req, @Nullable Link link) {
351+
if (link != null) {
352+
for (Map.Entry<String, String> entry : link.getHeader().entrySet()) {
353+
req.setHeader(entry.getKey(), entry.getValue());
354+
}
355+
}
356+
}
357+
361358
@NotNull
362359
public Lock lock(@NotNull String path, @Nullable Ref ref) throws IOException, LockConflictException {
363360
final LockCreate.Res res = doWork(auth -> doRequest(

gitlfs-common/src/main/java/ru/bozaro/gitlfs/common/data/Meta.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.fasterxml.jackson.annotation.JsonProperty;
55
import org.jetbrains.annotations.NotNull;
66

7+
import java.util.Objects;
8+
79
/**
810
* LFS object ___location.
911
*
@@ -34,4 +36,18 @@ public String getOid() {
3436
public long getSize() {
3537
return size;
3638
}
39+
40+
@Override
41+
public int hashCode() {
42+
return Objects.hash(oid, size);
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (!(o instanceof Meta))
48+
return false;
49+
50+
final Meta other = (Meta) o;
51+
return size == other.size && oid.equals(other.oid);
52+
}
3753
}

gitlfs-server/src/main/java/ru/bozaro/gitlfs/server/ContentServlet.java

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.jetbrains.annotations.NotNull;
44
import ru.bozaro.gitlfs.common.Constants;
5+
import ru.bozaro.gitlfs.common.JsonHelper;
56
import ru.bozaro.gitlfs.common.data.Meta;
67
import ru.bozaro.gitlfs.common.io.InputStreamValidator;
78
import ru.bozaro.gitlfs.server.internal.ObjectResponse;
@@ -15,6 +16,9 @@
1516
import java.io.InputStream;
1617
import java.util.regex.Pattern;
1718

19+
import static ru.bozaro.gitlfs.server.PointerServlet.checkMimeTypes;
20+
import static ru.bozaro.gitlfs.server.PointerServlet.sendError;
21+
1822
/**
1923
* Servlet for content storage.
2024
*
@@ -38,12 +42,35 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
3842
return;
3943
}
4044
} catch (ServerError e) {
41-
PointerServlet.sendError(resp, e);
45+
sendError(resp, e);
4246
return;
4347
}
4448
super.doGet(req, resp);
4549
}
4650

51+
@Override
52+
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
53+
try {
54+
checkMimeTypes(req);
55+
56+
if ((req.getPathInfo() != null) && PATTERN_OID.matcher(req.getPathInfo()).matches())
57+
processObjectVerify(req, req.getPathInfo().substring(1)).write(resp);
58+
} catch (ServerError e) {
59+
sendError(resp, e);
60+
}
61+
}
62+
63+
@NotNull
64+
private ResponseWriter processObjectVerify(@NotNull HttpServletRequest req, @NotNull String oid) throws IOException, ServerError {
65+
manager.checkUploadAccess(req);
66+
final Meta expectedMeta = JsonHelper.mapper.readValue(req.getInputStream(), Meta.class);
67+
final Meta actualMeta = manager.getMetadata(oid);
68+
if (!expectedMeta.equals(actualMeta))
69+
throw new ServerError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, String.format("LFS verification failure: server=%s client=%s", expectedMeta, actualMeta), null);
70+
71+
return response -> response.setStatus(HttpServletResponse.SC_OK);
72+
}
73+
4774
@Override
4875
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
4976
try {
@@ -52,7 +79,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws Se
5279
return;
5380
}
5481
} catch (ServerError e) {
55-
PointerServlet.sendError(resp, e);
82+
sendError(resp, e);
5683
return;
5784
}
5885
super.doPut(req, resp);
@@ -70,22 +97,19 @@ private ResponseWriter processPut(@NotNull HttpServletRequest req, @NotNull Stri
7097
private ResponseWriter processGet(@NotNull HttpServletRequest req, @NotNull String oid) throws ServerError, IOException {
7198
final ContentManager.Downloader downloader = manager.checkDownloadAccess(req);
7299
final InputStream stream = downloader.openObject(oid);
73-
return new ResponseWriter() {
74-
@Override
75-
public void write(@NotNull HttpServletResponse response) throws IOException {
76-
response.setStatus(HttpServletResponse.SC_OK);
77-
response.setContentType(Constants.MIME_BINARY);
78-
//noinspection TryFinallyCanBeTryWithResources
79-
try {
80-
byte[] buffer = new byte[0x10000];
81-
while (true) {
82-
final int read = stream.read(buffer);
83-
if (read < 0) break;
84-
response.getOutputStream().write(buffer, 0, read);
85-
}
86-
} finally {
87-
stream.close();
100+
return response -> {
101+
response.setStatus(HttpServletResponse.SC_OK);
102+
response.setContentType(Constants.MIME_BINARY);
103+
//noinspection TryFinallyCanBeTryWithResources
104+
try {
105+
byte[] buffer = new byte[0x10000];
106+
while (true) {
107+
final int read = stream.read(buffer);
108+
if (read < 0) break;
109+
response.getOutputStream().write(buffer, 0, read);
88110
}
111+
} finally {
112+
stream.close();
89113
}
90114
};
91115
}

0 commit comments

Comments
 (0)