mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #38 from overleaf/msw-disabled-projects
Report write-latex API 4xx errors in a friendly way to users
This commit is contained in:
commit
3c86eb0d52
10 changed files with 208 additions and 18 deletions
|
@ -35,6 +35,7 @@ import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook;
|
||||||
import uk.ac.ic.wlgitbridge.server.FileHandler;
|
import uk.ac.ic.wlgitbridge.server.FileHandler;
|
||||||
import uk.ac.ic.wlgitbridge.server.PostbackContents;
|
import uk.ac.ic.wlgitbridge.server.PostbackContents;
|
||||||
import uk.ac.ic.wlgitbridge.server.PostbackHandler;
|
import uk.ac.ic.wlgitbridge.server.PostbackHandler;
|
||||||
|
import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment;
|
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.push.PostbackManager;
|
import uk.ac.ic.wlgitbridge.snapshot.push.PostbackManager;
|
||||||
|
@ -383,6 +384,7 @@ public class Bridge {
|
||||||
* @param hostname
|
* @param hostname
|
||||||
* @throws SnapshotPostException
|
* @throws SnapshotPostException
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
|
* @throws MissingRepositoryException
|
||||||
* @throws ForbiddenException
|
* @throws ForbiddenException
|
||||||
*/
|
*/
|
||||||
public void push(
|
public void push(
|
||||||
|
@ -391,7 +393,7 @@ public class Bridge {
|
||||||
RawDirectory directoryContents,
|
RawDirectory directoryContents,
|
||||||
RawDirectory oldDirectoryContents,
|
RawDirectory oldDirectoryContents,
|
||||||
String hostname
|
String hostname
|
||||||
) throws SnapshotPostException, IOException, ForbiddenException {
|
) throws SnapshotPostException, IOException, MissingRepositoryException, ForbiddenException {
|
||||||
try (LockGuard __ = lock.lockGuard(projectName)) {
|
try (LockGuard __ = lock.lockGuard(projectName)) {
|
||||||
pushCritical(
|
pushCritical(
|
||||||
oauth2,
|
oauth2,
|
||||||
|
@ -460,6 +462,7 @@ public class Bridge {
|
||||||
* @param directoryContents
|
* @param directoryContents
|
||||||
* @param oldDirectoryContents
|
* @param oldDirectoryContents
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
|
* @throws MissingRepositoryException
|
||||||
* @throws ForbiddenException
|
* @throws ForbiddenException
|
||||||
* @throws SnapshotPostException
|
* @throws SnapshotPostException
|
||||||
*/
|
*/
|
||||||
|
@ -468,7 +471,7 @@ public class Bridge {
|
||||||
String projectName,
|
String projectName,
|
||||||
RawDirectory directoryContents,
|
RawDirectory directoryContents,
|
||||||
RawDirectory oldDirectoryContents
|
RawDirectory oldDirectoryContents
|
||||||
) throws IOException, ForbiddenException, SnapshotPostException {
|
) throws IOException, MissingRepositoryException, ForbiddenException, SnapshotPostException {
|
||||||
Log.info("[{}] Pushing", projectName);
|
Log.info("[{}] Pushing", projectName);
|
||||||
String postbackKey = postbackManager.makeKeyForProject(projectName);
|
String postbackKey = postbackManager.makeKeyForProject(projectName);
|
||||||
Log.info(
|
Log.info(
|
||||||
|
|
|
@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.bridge.snapshot;
|
||||||
|
|
||||||
import com.google.api.client.auth.oauth2.Credential;
|
import com.google.api.client.auth.oauth2.Credential;
|
||||||
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
||||||
|
import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
||||||
|
@ -33,13 +34,14 @@ public interface SnapshotApi {
|
||||||
String postbackKey);
|
String postbackKey);
|
||||||
|
|
||||||
static <T> T getResult(CompletableFuture<T> result)
|
static <T> T getResult(CompletableFuture<T> result)
|
||||||
throws FailedConnectionException, ForbiddenException {
|
throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
|
||||||
try {
|
try {
|
||||||
return result.join();
|
return result.join();
|
||||||
} catch (CompletionException e) {
|
} catch (CompletionException e) {
|
||||||
try {
|
try {
|
||||||
throw e.getCause();
|
throw e.getCause();
|
||||||
} catch (FailedConnectionException
|
} catch (MissingRepositoryException
|
||||||
|
| FailedConnectionException
|
||||||
| ForbiddenException
|
| ForbiddenException
|
||||||
| RuntimeException r) {
|
| RuntimeException r) {
|
||||||
throw r;
|
throw r;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.google.api.client.auth.oauth2.Credential;
|
||||||
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
||||||
import uk.ac.ic.wlgitbridge.data.model.Snapshot;
|
import uk.ac.ic.wlgitbridge.data.model.Snapshot;
|
||||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||||
|
import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
||||||
|
@ -65,7 +66,7 @@ public class SnapshotApiFacade {
|
||||||
Optional<Credential> oauth2,
|
Optional<Credential> oauth2,
|
||||||
CandidateSnapshot candidateSnapshot,
|
CandidateSnapshot candidateSnapshot,
|
||||||
String postbackKey
|
String postbackKey
|
||||||
) throws FailedConnectionException, ForbiddenException {
|
) throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
|
||||||
return SnapshotApi.getResult(api.push(
|
return SnapshotApi.getResult(api.push(
|
||||||
oauth2, candidateSnapshot, postbackKey));
|
oauth2, candidateSnapshot, postbackKey));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.apache.commons.codec.binary.Base64;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
import uk.ac.ic.wlgitbridge.application.config.Oauth2;
|
import uk.ac.ic.wlgitbridge.application.config.Oauth2;
|
||||||
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi;
|
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi;
|
||||||
|
import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest;
|
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest;
|
||||||
import uk.ac.ic.wlgitbridge.util.Instance;
|
import uk.ac.ic.wlgitbridge.util.Instance;
|
||||||
|
@ -76,6 +77,8 @@ public class Oauth2Filter implements Filter {
|
||||||
filterChain
|
filterChain
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} catch (MissingRepositoryException e) {
|
||||||
|
handleMissingRepository(project, e, (HttpServletResponse) servletResponse);
|
||||||
}
|
}
|
||||||
Log.info("[{}] Auth not needed", project);
|
Log.info("[{}] Auth not needed", project);
|
||||||
filterChain.doFilter(servletRequest, servletResponse);
|
filterChain.doFilter(servletRequest, servletResponse);
|
||||||
|
@ -130,7 +133,7 @@ public class Oauth2Filter implements Filter {
|
||||||
)
|
)
|
||||||
).execute().getAccessToken();
|
).execute().getAccessToken();
|
||||||
} catch (TokenResponseException e) {
|
} catch (TokenResponseException e) {
|
||||||
unauthorized(projectName, capturedUsername, e.getStatusCode(), request, response);
|
handleNeedAuthorization(projectName, capturedUsername, e.getStatusCode(), request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Credential cred = new Credential.Builder(
|
final Credential cred = new Credential.Builder(
|
||||||
|
@ -145,7 +148,7 @@ public class Oauth2Filter implements Filter {
|
||||||
servletResponse
|
servletResponse
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
unauthorized(projectName, capturedUsername, 0, request, response);
|
handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
throw new Error("Couldn't retrieve authentication", e);
|
throw new Error("Couldn't retrieve authentication", e);
|
||||||
|
@ -153,14 +156,14 @@ public class Oauth2Filter implements Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unauthorized(projectName, capturedUsername, 0, request, response);
|
handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() {}
|
public void destroy() {}
|
||||||
|
|
||||||
private void unauthorized(
|
private void handleNeedAuthorization(
|
||||||
String projectName,
|
String projectName,
|
||||||
String userName,
|
String userName,
|
||||||
int statusCode,
|
int statusCode,
|
||||||
|
@ -200,4 +203,23 @@ public class Oauth2Filter implements Filter {
|
||||||
w.close();
|
w.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleMissingRepository(
|
||||||
|
String projectName,
|
||||||
|
MissingRepositoryException e,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws IOException {
|
||||||
|
Log.info("[{}] Project missing.", projectName);
|
||||||
|
|
||||||
|
response.setContentType("text/plain");
|
||||||
|
|
||||||
|
// git special-cases 404 to give "repository '%s' not found",
|
||||||
|
// rather than displaying the raw status code.
|
||||||
|
response.setStatus(404);
|
||||||
|
|
||||||
|
PrintWriter w = response.getWriter();
|
||||||
|
for (String line : e.getDescriptionLines()) {
|
||||||
|
w.println(line);
|
||||||
|
}
|
||||||
|
w.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package uk.ac.ic.wlgitbridge.snapshot.base;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import uk.ac.ic.wlgitbridge.git.exception.SnapshotAPIException;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MissingRepositoryException extends SnapshotAPIException {
|
||||||
|
|
||||||
|
public static final List<String> GENERIC_REASON = Arrays.asList(
|
||||||
|
"This Overleaf project currently has no git access.",
|
||||||
|
"",
|
||||||
|
"If this problem persists, please contact us."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final List<String> EXPORTED_TO_V2 = Arrays.asList(
|
||||||
|
"This Overleaf project has been moved to Overleaf v2, and git access is temporarily unsupported.",
|
||||||
|
"",
|
||||||
|
"See https://www.overleaf.com/help/342 for more information."
|
||||||
|
);
|
||||||
|
|
||||||
|
private List<String> descriptionLines;
|
||||||
|
|
||||||
|
public MissingRepositoryException() {
|
||||||
|
descriptionLines = new ArrayList<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingRepositoryException(List<String> descriptionLines) {
|
||||||
|
this.descriptionLines = descriptionLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromJSON(JsonElement json) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "no git access";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getDescriptionLines() {
|
||||||
|
return this.descriptionLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.snapshot.base;
|
||||||
|
|
||||||
import com.google.api.client.http.*;
|
import com.google.api.client.http.*;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.ning.http.client.AsyncHttpClient;
|
import com.ning.http.client.AsyncHttpClient;
|
||||||
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
||||||
import uk.ac.ic.wlgitbridge.util.Instance;
|
import uk.ac.ic.wlgitbridge.util.Instance;
|
||||||
|
@ -50,7 +51,7 @@ public abstract class Request<T extends Result> {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T getResult() throws FailedConnectionException, ForbiddenException {
|
private T getResult() throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
|
||||||
try {
|
try {
|
||||||
HttpResponse response = future.get();
|
HttpResponse response = future.get();
|
||||||
Log.info(
|
Log.info(
|
||||||
|
@ -68,12 +69,30 @@ public abstract class Request<T extends Result> {
|
||||||
throw new FailedConnectionException();
|
throw new FailedConnectionException();
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof HttpResponseException &&
|
if (cause instanceof HttpResponseException) {
|
||||||
(((HttpResponseException) cause).getStatusCode() ==
|
HttpResponseException httpCause = (HttpResponseException) cause;
|
||||||
HttpServletResponse.SC_UNAUTHORIZED ||
|
int sc = httpCause.getStatusCode();
|
||||||
((HttpResponseException) cause).getStatusCode() ==
|
if (sc == HttpServletResponse.SC_UNAUTHORIZED || sc == HttpServletResponse.SC_FORBIDDEN) {
|
||||||
HttpServletResponse.SC_FORBIDDEN)) {
|
throw new ForbiddenException();
|
||||||
throw new ForbiddenException();
|
} else if (sc == HttpServletResponse.SC_NOT_FOUND) {
|
||||||
|
try {
|
||||||
|
JsonObject json = Instance.gson.fromJson(httpCause.getContent(), JsonObject.class);
|
||||||
|
String message = json.get("message").getAsString();
|
||||||
|
|
||||||
|
if ("Exported to v2".equals(message)) {
|
||||||
|
throw new MissingRepositoryException(MissingRepositoryException.EXPORTED_TO_V2);
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException
|
||||||
|
| ClassCastException
|
||||||
|
| NullPointerException _) {
|
||||||
|
// disregard any errors that arose while handling the JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingRepositoryException();
|
||||||
|
} else if (sc >= 400 && sc < 500) {
|
||||||
|
throw new MissingRepositoryException(MissingRepositoryException.GENERIC_REASON);
|
||||||
|
}
|
||||||
|
throw new FailedConnectionException(cause);
|
||||||
} else {
|
} else {
|
||||||
throw new FailedConnectionException(cause);
|
throw new FailedConnectionException(cause);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,14 @@ public class WLGitBridgeIntegrationTest {
|
||||||
put("canCloneMultipleRepositories", new HashMap<String, SnapshotAPIState>() {{
|
put("canCloneMultipleRepositories", new HashMap<String, SnapshotAPIState>() {{
|
||||||
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build());
|
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build());
|
||||||
}});
|
}});
|
||||||
put("cannotCloneAProtectedProject", new HashMap<String, SnapshotAPIState>() {{
|
put("cannotCloneAProtectedProjectWithoutAuthentication", new HashMap<String, SnapshotAPIState>() {{
|
||||||
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProject/state/state.json")).build());
|
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json")).build());
|
||||||
|
}});
|
||||||
|
put("cannotCloneA4xxProject", new HashMap<String, SnapshotAPIState>() {{
|
||||||
|
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneA4xxProject/state/state.json")).build());
|
||||||
|
}});
|
||||||
|
put("cannotCloneAMissingProject", new HashMap<String, SnapshotAPIState>() {{
|
||||||
|
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAMissingProject/state/state.json")).build());
|
||||||
}});
|
}});
|
||||||
put("canPullAModifiedTexFile", new HashMap<String, SnapshotAPIState>() {{
|
put("canPullAModifiedTexFile", new HashMap<String, SnapshotAPIState>() {{
|
||||||
put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build());
|
put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build());
|
||||||
|
@ -727,6 +733,60 @@ public class WLGitBridgeIntegrationTest {
|
||||||
wlgb.stop();
|
wlgb.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cannotCloneAProtectedProjectWithoutAuthentication() throws IOException, GitAPIException, InterruptedException {
|
||||||
|
int gitBridgePort = 33883;
|
||||||
|
int mockServerPort = 3883;
|
||||||
|
|
||||||
|
MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAProtectedProjectWithoutAuthentication").toFile());
|
||||||
|
server.start();
|
||||||
|
server.setState(states.get("cannotCloneAProtectedProjectWithoutAuthentication").get("state"));
|
||||||
|
GitBridgeApp wlgb = new GitBridgeApp(new String[] {
|
||||||
|
makeConfigFile(gitBridgePort, mockServerPort)
|
||||||
|
});
|
||||||
|
|
||||||
|
wlgb.run();
|
||||||
|
Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
|
||||||
|
wlgb.stop();
|
||||||
|
assertNotEquals(0, gitProcess.waitFor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cannotCloneA4xxProject() throws IOException, GitAPIException, InterruptedException {
|
||||||
|
int gitBridgePort = 33879;
|
||||||
|
int mockServerPort = 3879;
|
||||||
|
|
||||||
|
MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneA4xxProject").toFile());
|
||||||
|
server.start();
|
||||||
|
server.setState(states.get("cannotCloneA4xxProject").get("state"));
|
||||||
|
GitBridgeApp wlgb = new GitBridgeApp(new String[] {
|
||||||
|
makeConfigFile(gitBridgePort, mockServerPort)
|
||||||
|
});
|
||||||
|
|
||||||
|
wlgb.run();
|
||||||
|
Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
|
||||||
|
wlgb.stop();
|
||||||
|
assertNotEquals(0, gitProcess.waitFor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cannotCloneAMissingProject() throws IOException, GitAPIException, InterruptedException {
|
||||||
|
int gitBridgePort = 33880;
|
||||||
|
int mockServerPort = 3880;
|
||||||
|
|
||||||
|
MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAMissingProject").toFile());
|
||||||
|
server.start();
|
||||||
|
server.setState(states.get("cannotCloneAMissingProject").get("state"));
|
||||||
|
GitBridgeApp wlgb = new GitBridgeApp(new String[] {
|
||||||
|
makeConfigFile(gitBridgePort, mockServerPort)
|
||||||
|
});
|
||||||
|
|
||||||
|
wlgb.run();
|
||||||
|
Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
|
||||||
|
wlgb.stop();
|
||||||
|
assertNotEquals(0, gitProcess.waitFor());
|
||||||
|
}
|
||||||
|
|
||||||
private String makeConfigFile(
|
private String makeConfigFile(
|
||||||
int port,
|
int port,
|
||||||
int apiPort
|
int apiPort
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"project": "gone",
|
||||||
|
"getDoc": {
|
||||||
|
"error": 410,
|
||||||
|
"versionID": 1,
|
||||||
|
"createdAt": "2018-02-05T15:30:00Z",
|
||||||
|
"email": "michael.walker@overleaf.com",
|
||||||
|
"name": "msw"
|
||||||
|
},
|
||||||
|
"getSavedVers": [],
|
||||||
|
"getForVers": [],
|
||||||
|
"push": "success",
|
||||||
|
"postback": {
|
||||||
|
"type": "outOfDate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"project": "missing",
|
||||||
|
"getDoc": {
|
||||||
|
"error": 404,
|
||||||
|
"versionID": 1,
|
||||||
|
"createdAt": "2018-02-06T13:29:00Z",
|
||||||
|
"email": "michael.walker@overleaf.com",
|
||||||
|
"name": "msw"
|
||||||
|
},
|
||||||
|
"getSavedVers": [],
|
||||||
|
"getForVers": [],
|
||||||
|
"push": "success",
|
||||||
|
"postback": {
|
||||||
|
"type": "outOfDate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in a new issue