mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
merge multiple repositories into an existing monorepo
- merged using: 'monorepo_add.sh services-git-bridge:services/git-bridge' - see https://github.com/shopsys/monorepo-tools
This commit is contained in:
commit
4fd44292d0
547 changed files with 20788 additions and 0 deletions
9
services/git-bridge/.dockerignore
Normal file
9
services/git-bridge/.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
|||
*
|
||||
!start.sh
|
||||
!/conf
|
||||
!/lib
|
||||
!/src/main
|
||||
!/pom.xml
|
||||
!/Makefile
|
||||
!/LICENSE
|
||||
!/vendor
|
21
services/git-bridge/.github/dependabot.yml
vendored
Normal file
21
services/git-bridge/.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
pull-request-branch-name:
|
||||
# Separate sections of the branch name with a hyphen
|
||||
# Docker images use the branch name and do not support slashes in tags
|
||||
# https://github.com/overleaf/google-ops/issues/822
|
||||
# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#pull-request-branch-nameseparator
|
||||
separator: "-"
|
||||
|
||||
# Block informal upgrades -- security upgrades use a separate queue.
|
||||
# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#open-pull-requests-limit
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "type:maintenance"
|
53
services/git-bridge/.gitignore
vendored
Normal file
53
services/git-bridge/.gitignore
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# Let's not share anything because we're using Maven.
|
||||
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
target/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Local configuration files
|
||||
conf/runtime.json
|
58
services/git-bridge/Dockerfile
Normal file
58
services/git-bridge/Dockerfile
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Dockerfile for git-bridge
|
||||
|
||||
FROM maven:3-jdk-11 as base
|
||||
|
||||
RUN apt-get update && apt-get install -y make git sqlite3 \
|
||||
&& rm -rf /var/lib/apt/lists
|
||||
|
||||
COPY vendor/envsubst /opt/envsubst
|
||||
RUN chmod +x /opt/envsubst
|
||||
|
||||
RUN useradd --create-home node
|
||||
|
||||
FROM base as builder
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN make package \
|
||||
# The name of the created jar contains the current version tag.
|
||||
# Rename it to a static path that can be used for copying.
|
||||
&& find /app/target \
|
||||
-name 'writelatex-git-bridge*jar-with-dependencies.jar' \
|
||||
-exec mv {} /git-bridge.jar \;
|
||||
|
||||
FROM openjdk:11-jre
|
||||
|
||||
RUN apt-get update && apt-get install -y git sqlite3 procps htop net-tools sockstat libjemalloc2 \
|
||||
&& rm -rf /var/lib/apt/lists
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
|
||||
|
||||
# Install Google Cloud Profiler agent
|
||||
RUN mkdir -p /opt/cprof && \
|
||||
wget -q -O- https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz \
|
||||
| tar xzv -C /opt/cprof
|
||||
|
||||
# Install Google Cloud Debugger agent
|
||||
RUN mkdir /opt/cdbg && \
|
||||
wget -qO- https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz | \
|
||||
tar xvz -C /opt/cdbg
|
||||
|
||||
RUN useradd --create-home node
|
||||
|
||||
COPY --from=builder /git-bridge.jar /
|
||||
|
||||
COPY vendor/envsubst /opt/envsubst
|
||||
RUN chmod +x /opt/envsubst
|
||||
|
||||
COPY conf/envsubst_template.json envsubst_template.json
|
||||
COPY start.sh start.sh
|
||||
|
||||
RUN mkdir conf
|
||||
RUN chown node:node conf
|
||||
|
||||
USER node
|
||||
|
||||
CMD ["/start.sh"]
|
22
services/git-bridge/LICENSE
Normal file
22
services/git-bridge/LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Winston Li
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
31
services/git-bridge/Makefile
Normal file
31
services/git-bridge/Makefile
Normal file
|
@ -0,0 +1,31 @@
|
|||
# git-bridge makefile
|
||||
|
||||
MVN_OPTS := "--no-transfer-progress"
|
||||
|
||||
runtime-conf:
|
||||
/opt/envsubst < conf/envsubst_template.json > conf/runtime.json
|
||||
|
||||
|
||||
run: package runtime-conf
|
||||
java $(GIT_BRIDGE_JVM_ARGS) -jar \
|
||||
target/writelatex-git-bridge-1.0-SNAPSHOT-jar-with-dependencies.jar \
|
||||
conf/runtime.json
|
||||
|
||||
|
||||
build:
|
||||
mvn $(MVN_OPTS) package -DskipTests
|
||||
|
||||
|
||||
test:
|
||||
mvn $(MVN_OPTS) test
|
||||
|
||||
|
||||
clean:
|
||||
mvn $(MVN_OPTS) clean
|
||||
|
||||
|
||||
package: clean
|
||||
mvn $(MVN_OPTS) package -DskipTests
|
||||
|
||||
|
||||
.PHONY: run package build clean test runtime-conf
|
138
services/git-bridge/README.md
Normal file
138
services/git-bridge/README.md
Normal file
|
@ -0,0 +1,138 @@
|
|||
# writelatex-git-bridge
|
||||
|
||||
## Docker
|
||||
|
||||
The `Dockerfile` contains all the requirements for building and running the
|
||||
writelatex-git-bridge.
|
||||
|
||||
```bash
|
||||
# build the image
|
||||
docker build -t writelatex-git-bridge .
|
||||
|
||||
# run it with the demo config
|
||||
docker run -v `pwd`/conf/local.json:/conf/runtime.json writelatex-git-bridge
|
||||
```
|
||||
|
||||
## Native install
|
||||
|
||||
### Required packages
|
||||
|
||||
* `maven` (for building, running tests and packaging)
|
||||
* `jdk-8` (for compiling and running)
|
||||
|
||||
### Commands
|
||||
|
||||
To be run from the base directory:
|
||||
|
||||
**Build jar**:
|
||||
`mvn package`
|
||||
|
||||
**Run tests**:
|
||||
`mvn test`
|
||||
|
||||
**Clean**:
|
||||
`mvn clean`
|
||||
|
||||
To be run from the dev-environment:
|
||||
|
||||
**Build jar**:
|
||||
`bin/run git-bridge make package`
|
||||
|
||||
**Run tests**:
|
||||
`bin/run git-bridge make test`
|
||||
|
||||
**Clean**:
|
||||
`bin/run git-bridge make clean`
|
||||
|
||||
### Installation
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y maven
|
||||
sudo apt-get install -y openjdk-8-jdk
|
||||
sudo update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
|
||||
sudo update-alternatives --set javac /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/javac
|
||||
```
|
||||
|
||||
Create a config file according to the format below.
|
||||
|
||||
Run `mvn package` to build, test, and package it into a jar at `target/writelatex-git-bridge-1.0-SNAPSHOT-jar-with-dependencies.jar`.
|
||||
|
||||
Use `java -jar <path_to_jar> <path_to_config_file>` to run the server.
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
The configuration file is in `.json` format.
|
||||
|
||||
{
|
||||
"port" (int): the port number,
|
||||
"rootGitDirectory" (string): the directory in which to store
|
||||
git repos and the db/atts,
|
||||
"apiBaseUrl" (string): base url for the snapshot api,
|
||||
"username" (string, optional): username for http basic auth,
|
||||
"password" (string, optional): password for http basic auth,
|
||||
"postbackBaseUrl" (string): the postback url,
|
||||
"serviceName" (string): current name of writeLaTeX
|
||||
in case it ever changes,
|
||||
"oauth2" (object): { null or missing if oauth2 shouldn't be used
|
||||
"oauth2ClientID" (string): oauth2 client ID,
|
||||
"oauth2ClientSecret" (string): oauth2 client secret,
|
||||
"oauth2Server" (string): oauth2 server,
|
||||
with protocol and
|
||||
without trailing slash
|
||||
},
|
||||
"repoStore" (object, optional): { configure the repo store
|
||||
"maxFileSize" (long, optional): maximum size of a file, inclusive
|
||||
},
|
||||
"swapStore" (object, optional): { the place to swap projects to.
|
||||
if null, type defaults to
|
||||
"noop"
|
||||
"type" (string): "s3", "memory", "noop" (not recommended),
|
||||
"awsAccessKey" (string, optional): only for s3,
|
||||
"awsSecret" (string, optional): only for s3,
|
||||
"s3BucketName" (string, optional): only for s3
|
||||
},
|
||||
"swapJob" (object, optional): { configure the project
|
||||
swapping job.
|
||||
if null, defaults to no-op
|
||||
"minProjects" (int64): lower bound on number of projects
|
||||
present. The swap job will never go
|
||||
below this, regardless of what the
|
||||
watermark shows. Regardless, if
|
||||
minProjects prevents an eviction,
|
||||
the swap job will WARN,
|
||||
"lowGiB" (int32): the low watermark for swapping,
|
||||
i.e. swap until disk usage is below this,
|
||||
"highGiB" (int32): the high watermark for swapping,
|
||||
i.e. start swapping when
|
||||
disk usage becomes this,
|
||||
"intervalMillis" (int64): amount of time in between running
|
||||
swap job and checking watermarks.
|
||||
3600000 is 1 hour
|
||||
}
|
||||
}
|
||||
|
||||
You have to restart the server for configuration changes to take effect.
|
||||
|
||||
|
||||
## Creating OAuth app
|
||||
|
||||
In dev-env, run the following command in mongo to create the oauth application
|
||||
for git-bridge.
|
||||
|
||||
```
|
||||
db.oauthApplications.insert({
|
||||
"clientSecret" : "e6b2e9eee7ae2bb653823250bb69594a91db0547fe3790a7135acb497108e62d",
|
||||
"grants" : [
|
||||
"password"
|
||||
],
|
||||
"id" : "264c723c925c13590880751f861f13084934030c13b4452901e73bdfab226edc",
|
||||
"name" : "Overleaf Git Bridge",
|
||||
"redirectUris" : [],
|
||||
"scopes" : [
|
||||
"git_bridge"
|
||||
]
|
||||
})
|
||||
```
|
33
services/git-bridge/conf/envsubst_template.json
Normal file
33
services/git-bridge/conf/envsubst_template.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"port": ${GIT_BRIDGE_PORT:-8000},
|
||||
"bindIp": "${GIT_BRIDGE_BIND_IP:-0.0.0.0}",
|
||||
"idleTimeout": ${GIT_BRIDGE_IDLE_TIMEOUT:-30000},
|
||||
"rootGitDirectory": "${GIT_BRIDGE_ROOT_DIR:-/tmp/wlgb}",
|
||||
"apiBaseUrl": "${GIT_BRIDGE_API_BASE_URL:-https://localhost/api/v0}",
|
||||
"postbackBaseUrl": "${GIT_BRIDGE_POSTBACK_BASE_URL:-https://localhost}",
|
||||
"serviceName": "${GIT_BRIDGE_SERVICE_NAME:-Overleaf}",
|
||||
"oauth2": {
|
||||
"oauth2ClientID": "${GIT_BRIDGE_OAUTH2_CLIENT_ID}",
|
||||
"oauth2ClientSecret": "${GIT_BRIDGE_OAUTH2_CLIENT_SECRET}",
|
||||
"oauth2Server": "${GIT_BRIDGE_OAUTH2_SERVER:-https://localhost}"
|
||||
},
|
||||
"repoStore": {
|
||||
"maxFileNum": ${GIT_BRIDGE_REPOSTORE_MAX_FILE_NUM:-2000},
|
||||
"maxFileSize": ${GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE:-52428800}
|
||||
},
|
||||
"swapStore": {
|
||||
"type": "${GIT_BRIDGE_SWAPSTORE_TYPE:-noop}",
|
||||
"awsAccessKey": "${GIT_BRIDGE_SWAPSTORE_AWS_ACCESS_KEY}",
|
||||
"awsSecret": "${GIT_BRIDGE_SWAPSTORE_AWS_SECRET}",
|
||||
"s3BucketName": "${GIT_BRIDGE_SWAPSTORE_S3_BUCKET_NAME}",
|
||||
"awsRegion": "${GIT_BRIDGE_SWAPSTORE_AWS_REGION:-us-east-1}"
|
||||
},
|
||||
"swapJob": {
|
||||
"minProjects": ${GIT_BRIDGE_SWAPJOB_MIN_PROJECTS:-50},
|
||||
"lowGiB": ${GIT_BRIDGE_SWAPJOB_LOW_GIB:-128},
|
||||
"highGiB": ${GIT_BRIDGE_SWAPJOB_HIGH_GIB:-256},
|
||||
"intervalMillis": ${GIT_BRIDGE_SWAPJOB_INTERVAL_MILLIS:-3600000},
|
||||
"compressionMethod": "${GIT_BRIDGE_SWAPJOB_COMPRESSION_METHOD:-gzip}"
|
||||
},
|
||||
"sqliteHeapLimitBytes": ${GIT_BRIDGE_SQLITE_HEAP_LIMIT_BYTES:-0}
|
||||
}
|
33
services/git-bridge/conf/example_config.json
Normal file
33
services/git-bridge/conf/example_config.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"port": 8080,
|
||||
"bindIp": "127.0.0.1",
|
||||
"idleTimeout": 30000,
|
||||
"rootGitDirectory": "/tmp/wlgb",
|
||||
"apiBaseUrl": "https://localhost/api/v0",
|
||||
"postbackBaseUrl": "https://localhost",
|
||||
"serviceName": "Overleaf",
|
||||
"oauth2": {
|
||||
"oauth2ClientID": "asdf",
|
||||
"oauth2ClientSecret": "asdf",
|
||||
"oauth2Server": "https://localhost"
|
||||
},
|
||||
"repoStore": {
|
||||
"maxFileNum": 2000,
|
||||
"maxFileSize": 52428800
|
||||
},
|
||||
"swapStore": {
|
||||
"type": "s3",
|
||||
"awsAccessKey": "asdf",
|
||||
"awsSecret": "asdf",
|
||||
"s3BucketName": "com.overleaf.testbucket",
|
||||
"awsRegion": "us-east-1"
|
||||
},
|
||||
"swapJob": {
|
||||
"minProjects": 50,
|
||||
"lowGiB": 128,
|
||||
"highGiB": 256,
|
||||
"intervalMillis": 3600000,
|
||||
"compressionMethod": "gzip"
|
||||
},
|
||||
"sqliteHeapLimitBytes": 512000000
|
||||
}
|
28
services/git-bridge/conf/local.json
Normal file
28
services/git-bridge/conf/local.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"port": 8000,
|
||||
"bindIp": "0.0.0.0",
|
||||
"idleTimeout": 30000,
|
||||
"rootGitDirectory": "/tmp/wlgb",
|
||||
"apiBaseUrl": "http://v2.overleaf.test:4000/api/v0",
|
||||
"postbackBaseUrl": "http://git-bridge:8000",
|
||||
"serviceName": "Overleaf",
|
||||
"oauth2": {
|
||||
"oauth2ClientID": "264c723c925c13590880751f861f13084934030c13b4452901e73bdfab226edc",
|
||||
"oauth2ClientSecret": "e6b2e9eee7ae2bb653823250bb69594a91db0547fe3790a7135acb497108e62d",
|
||||
"oauth2Server": "http://v2.overleaf.test:4000"
|
||||
},
|
||||
"repoStore": {
|
||||
"maxFileNum": 2000,
|
||||
"maxFileSize": 52428800
|
||||
},
|
||||
"swapStore": {
|
||||
"type": "noop"
|
||||
},
|
||||
"swapJob": {
|
||||
"minProjects": 50,
|
||||
"lowGiB": 128,
|
||||
"highGiB": 256,
|
||||
"intervalMillis": 3600000,
|
||||
"compressionMethod": "gzip"
|
||||
}
|
||||
}
|
BIN
services/git-bridge/lib/newrelic.jar
Normal file
BIN
services/git-bridge/lib/newrelic.jar
Normal file
Binary file not shown.
300
services/git-bridge/newrelic.yml
Normal file
300
services/git-bridge/newrelic.yml
Normal file
|
@ -0,0 +1,300 @@
|
|||
# This file configures the New Relic Agent. New Relic monitors
|
||||
# Java applications with deep visibility and low overhead. For more details and additional
|
||||
# configuration options visit https://docs.newrelic.com/docs/java/java-agent-configuration.
|
||||
#
|
||||
# This configuration file is custom generated for Winston
|
||||
#
|
||||
# This section is for settings common to all environments.
|
||||
# Do not add anything above this next line.
|
||||
common: &default_settings
|
||||
|
||||
# ============================== LICENSE KEY ===============================
|
||||
# You must specify the license key associated with your New Relic
|
||||
# account. For example, if your license key is 12345 use this:
|
||||
# license_key: '12345'
|
||||
# The key binds your Agent's data to your account in the New Relic service.
|
||||
license_key: '<LICENSE KEY>'
|
||||
|
||||
# Agent Enabled
|
||||
# Use this setting to disable the agent instead of removing it from the startup command.
|
||||
# Default is true.
|
||||
agent_enabled: true
|
||||
|
||||
# Set the name of your application as you'd like it show up in New Relic.
|
||||
# If enable_auto_app_naming is false, the agent reports all data to this application.
|
||||
# Otherwise, the agent reports only background tasks (transactions for non-web applications)
|
||||
# to this application. To report data to more than one application
|
||||
# (useful for rollup reporting), separate the application names with ";".
|
||||
# For example, to report data to "My Application" and "My Application 2" use this:
|
||||
# app_name: My Application;My Application 2
|
||||
# This setting is required. Up to 3 different application names can be specified.
|
||||
# The first application name must be unique.
|
||||
app_name: Git Bridge
|
||||
|
||||
# To enable high security, set this property to true. When in high
|
||||
# security mode, the agent will use SSL and obfuscated SQL. Additionally,
|
||||
# request parameters and message parameters will not be sent to New Relic.
|
||||
high_security: false
|
||||
|
||||
# Set to true to enable support for auto app naming.
|
||||
# The name of each web app is detected automatically
|
||||
# and the agent reports data separately for each one.
|
||||
# This provides a finer-grained performance breakdown for
|
||||
# web apps in New Relic.
|
||||
# Default is false.
|
||||
enable_auto_app_naming: false
|
||||
|
||||
# Set to true to enable component-based transaction naming.
|
||||
# Set to false to use the URI of a web request as the name of the transaction.
|
||||
# Default is true.
|
||||
enable_auto_transaction_naming: true
|
||||
|
||||
# The agent uses its own log file to keep its logging
|
||||
# separate from that of your application. Specify the log level here.
|
||||
# This setting is dynamic, so changes do not require restarting your application.
|
||||
# The levels in increasing order of verboseness are:
|
||||
# off, severe, warning, info, fine, finer, finest
|
||||
# Default is info.
|
||||
log_level: info
|
||||
|
||||
# Log all data sent to and from New Relic in plain text.
|
||||
# This setting is dynamic, so changes do not require restarting your application.
|
||||
# Default is false.
|
||||
audit_mode: false
|
||||
|
||||
# The number of backup log files to save.
|
||||
# Default is 1.
|
||||
log_file_count: 1
|
||||
|
||||
# The maximum number of kbytes to write to any one log file.
|
||||
# The log_file_count must be set greater than 1.
|
||||
# Default is 0 (no limit).
|
||||
log_limit_in_kbytes: 0
|
||||
|
||||
# Override other log rolling configuration and roll the logs daily.
|
||||
# Default is false.
|
||||
log_daily: false
|
||||
|
||||
# The name of the log file.
|
||||
# Default is newrelic_agent.log.
|
||||
log_file_name: newrelic_agent.log
|
||||
|
||||
# The log file directory.
|
||||
# Default is the logs directory in the newrelic.jar parent directory.
|
||||
#log_file_path:
|
||||
|
||||
# The agent communicates with New Relic via https by
|
||||
# default. If you want to communicate with newrelic via http,
|
||||
# then turn off SSL by setting this value to false.
|
||||
# This work is done asynchronously to the threads that process your
|
||||
# application code, so response times will not be directly affected
|
||||
# by this change.
|
||||
# Default is true.
|
||||
ssl: true
|
||||
|
||||
# Proxy settings for connecting to the New Relic server:
|
||||
# If a proxy is used, the host setting is required. Other settings
|
||||
# are optional. Default port is 8080. The username and password
|
||||
# settings will be used to authenticate to Basic Auth challenges
|
||||
# from a proxy server.
|
||||
#proxy_host: hostname
|
||||
#proxy_port: 8080
|
||||
#proxy_user: username
|
||||
#proxy_password: password
|
||||
|
||||
# Limits the number of lines to capture for each stack trace.
|
||||
# Default is 30
|
||||
max_stack_trace_lines: 30
|
||||
|
||||
# Provides the ability to configure the attributes sent to New Relic. These
|
||||
# attributes can be found in transaction traces, traced errors, Insight's
|
||||
# transaction events, and Insight's page views.
|
||||
attributes:
|
||||
|
||||
# When true, attributes will be sent to New Relic. The default is true.
|
||||
enabled: true
|
||||
|
||||
#A comma separated list of attribute keys whose values should
|
||||
# be sent to New Relic.
|
||||
#include:
|
||||
|
||||
# A comma separated list of attribute keys whose values should
|
||||
# not be sent to New Relic.
|
||||
#exclude:
|
||||
|
||||
|
||||
# Transaction tracer captures deep information about slow
|
||||
# transactions and sends this to the New Relic service once a
|
||||
# minute. Included in the transaction is the exact call sequence of
|
||||
# the transactions including any SQL statements issued.
|
||||
transaction_tracer:
|
||||
|
||||
# Transaction tracer is enabled by default. Set this to false to turn it off.
|
||||
# This feature is not available to Lite accounts and is automatically disabled.
|
||||
# Default is true.
|
||||
enabled: true
|
||||
|
||||
# Threshold in seconds for when to collect a transaction
|
||||
# trace. When the response time of a controller action exceeds
|
||||
# this threshold, a transaction trace will be recorded and sent to
|
||||
# New Relic. Valid values are any float value, or (default) "apdex_f",
|
||||
# which will use the threshold for the "Frustrated" Apdex level
|
||||
# (greater than four times the apdex_t value).
|
||||
# Default is apdex_f.
|
||||
transaction_threshold: apdex_f
|
||||
|
||||
# When transaction tracer is on, SQL statements can optionally be
|
||||
# recorded. The recorder has three modes, "off" which sends no
|
||||
# SQL, "raw" which sends the SQL statement in its original form,
|
||||
# and "obfuscated", which strips out numeric and string literals.
|
||||
# Default is obfuscated.
|
||||
record_sql: obfuscated
|
||||
|
||||
# Set this to true to log SQL statements instead of recording them.
|
||||
# SQL is logged using the record_sql mode.
|
||||
# Default is false.
|
||||
log_sql: false
|
||||
|
||||
# Threshold in seconds for when to collect stack trace for a SQL
|
||||
# call. In other words, when SQL statements exceed this threshold,
|
||||
# then capture and send to New Relic the current stack trace. This is
|
||||
# helpful for pinpointing where long SQL calls originate from.
|
||||
# Default is 0.5 seconds.
|
||||
stack_trace_threshold: 0.5
|
||||
|
||||
# Determines whether the agent will capture query plans for slow
|
||||
# SQL queries. Only supported for MySQL and PostgreSQL.
|
||||
# Default is true.
|
||||
explain_enabled: true
|
||||
|
||||
# Threshold for query execution time below which query plans will not
|
||||
# not be captured. Relevant only when `explain_enabled` is true.
|
||||
# Default is 0.5 seconds.
|
||||
explain_threshold: 0.5
|
||||
|
||||
# Use this setting to control the variety of transaction traces.
|
||||
# The higher the setting, the greater the variety.
|
||||
# Set this to 0 to always report the slowest transaction trace.
|
||||
# Default is 20.
|
||||
top_n: 20
|
||||
|
||||
# Error collector captures information about uncaught exceptions and
|
||||
# sends them to New Relic for viewing.
|
||||
error_collector:
|
||||
|
||||
# This property enables the collection of errors. If the property is not
|
||||
# set or the property is set to false, then errors will not be collected.
|
||||
# Default is true.
|
||||
enabled: true
|
||||
|
||||
# Use this property to exclude specific exceptions from being reported as errors
|
||||
# by providing a comma separated list of full class names.
|
||||
# The default is to exclude akka.actor.ActorKilledException. If you want to override
|
||||
# this, you must provide any new value as an empty list is ignored.
|
||||
ignore_errors: akka.actor.ActorKilledException
|
||||
|
||||
# Use this property to exclude specific http status codes from being reported as errors
|
||||
# by providing a comma separated list of status codes.
|
||||
# The default is to exclude 404s. If you want to override
|
||||
# this, you must provide any new value as an empty list is ignored.
|
||||
ignore_status_codes: 404
|
||||
|
||||
# Transaction Events are used for Histograms and Percentiles. Unaggregated data is collected
|
||||
# for each web transaction and sent to the server on harvest.
|
||||
transaction_events:
|
||||
|
||||
# Set to false to disable transaction events.
|
||||
# Default is true.
|
||||
enabled: true
|
||||
|
||||
# Events are collected up to the configured amount. Afterwards, events are sampled to
|
||||
# maintain an even distribution across the harvest cycle.
|
||||
# Default is 2000. Setting to 0 will disable.
|
||||
max_samples_stored: 2000
|
||||
|
||||
# Cross Application Tracing adds request and response headers to
|
||||
# external calls using supported HTTP libraries to provide better
|
||||
# performance data when calling applications monitored by other New Relic Agents.
|
||||
cross_application_tracer:
|
||||
|
||||
# Set to false to disable cross application tracing.
|
||||
# Default is true.
|
||||
enabled: true
|
||||
|
||||
# Thread profiler measures wall clock time, CPU time, and method call counts
|
||||
# in your application's threads as they run.
|
||||
# This feature is not available to Lite accounts and is automatically disabled.
|
||||
thread_profiler:
|
||||
|
||||
# Set to false to disable the thread profiler.
|
||||
# Default is true.
|
||||
enabled: true
|
||||
|
||||
# New Relic Real User Monitoring gives you insight into the performance real users are
|
||||
# experiencing with your website. This is accomplished by measuring the time it takes for
|
||||
# your users' browsers to download and render your web pages by injecting a small amount
|
||||
# of JavaScript code into the header and footer of each page.
|
||||
browser_monitoring:
|
||||
|
||||
# By default the agent automatically inserts API calls in compiled JSPs to
|
||||
# inject the monitoring JavaScript into web pages. Not all rendering engines are supported.
|
||||
# See https://docs.newrelic.com/docs/java/real-user-monitoring-in-java#manual_instrumentation
|
||||
# for instructions to add these manually to your pages.
|
||||
# Set this attribute to false to turn off this behavior.
|
||||
auto_instrument: true
|
||||
|
||||
class_transformer:
|
||||
# This instrumentation reports the name of the user principal returned from
|
||||
# HttpServletRequest.getUserPrincipal() when servlets and filters are invoked.
|
||||
com.newrelic.instrumentation.servlet-user:
|
||||
enabled: false
|
||||
|
||||
com.newrelic.instrumentation.spring-aop-2:
|
||||
enabled: false
|
||||
|
||||
# Classes loaded by classloaders in this list will not be instrumented.
|
||||
# This is a useful optimization for runtimes which use classloaders to
|
||||
# load dynamic classes which the agent would not instrument.
|
||||
classloader_excludes:
|
||||
groovy.lang.GroovyClassLoader$InnerLoader,
|
||||
org.codehaus.groovy.runtime.callsite.CallSiteClassLoader,
|
||||
com.collaxa.cube.engine.deployment.BPELClassLoader,
|
||||
org.springframework.data.convert.ClassGeneratingEntityInstantiator$ObjectInstantiatorClassGenerator,
|
||||
org.mvel2.optimizers.impl.asm.ASMAccessorOptimizer$ContextClassLoader,
|
||||
gw.internal.gosu.compiler.SingleServingGosuClassLoader,
|
||||
|
||||
# User-configurable custom labels for this agent. Labels are name-value pairs.
|
||||
# There is a maximum of 64 labels per agent. Names and values are limited to 255 characters.
|
||||
# Names and values may not contain colons (:) or semicolons (;).
|
||||
labels:
|
||||
|
||||
# An example label
|
||||
#label_name: label_value
|
||||
|
||||
|
||||
# Application Environments
|
||||
# ------------------------------------------
|
||||
# Environment specific settings are in this section.
|
||||
# You can use the environment to override the default settings.
|
||||
# For example, to change the app_name setting.
|
||||
# Use -Dnewrelic.environment=<environment> on the Java startup command line
|
||||
# to set the environment.
|
||||
# The default environment is production.
|
||||
|
||||
# NOTE if your application has other named environments, you should
|
||||
# provide configuration settings for these environments here.
|
||||
|
||||
development:
|
||||
<<: *default_settings
|
||||
app_name: Git Bridge (Development)
|
||||
|
||||
test:
|
||||
<<: *default_settings
|
||||
app_name: Git Bridge (Test)
|
||||
|
||||
production:
|
||||
<<: *default_settings
|
||||
|
||||
staging:
|
||||
<<: *default_settings
|
||||
app_name: Git Bridge (Staging)
|
229
services/git-bridge/pom.xml
Normal file
229
services/git-bridge/pom.xml
Normal file
|
@ -0,0 +1,229 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>uk.ac.ic.wlgitbridge</groupId>
|
||||
<artifactId>writelatex-git-bridge</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!--<maven.test.skip>true</maven.test.skip>-->
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-compiler-plugin -->
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerArgument></compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Workaround, test loader crashes without this configuration option -->
|
||||
<!-- See: https://stackoverflow.com/questions/53010200/maven-surefire-could-not-find-forkedbooter-class -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-assembly-plugin -->
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>uk.ac.ic.wlgitbridge.Main</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/junit/junit -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.jmock/jmock-junit4 -->
|
||||
<dependency>
|
||||
<groupId>org.jmock</groupId>
|
||||
<artifactId>jmock-junit4</artifactId>
|
||||
<version>2.8.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlet -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>9.4.38.v20210224</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-server -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>9.4.38.v20210224</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.2</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.asynchttpclient/async-http-client -->
|
||||
<dependency>
|
||||
<groupId>org.asynchttpclient</groupId>
|
||||
<artifactId>async-http-client</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit</artifactId>
|
||||
<version>5.12.0.202106070339-r</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit.http.server -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit.http.server</artifactId>
|
||||
<version>5.12.0.202106070339-r</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.36.0.1</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
<version>2.9.9</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.oauth-client/google-oauth-client -->
|
||||
<dependency>
|
||||
<groupId>com.google.oauth-client</groupId>
|
||||
<artifactId>google-oauth-client</artifactId>
|
||||
<version>1.23.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.http-client/google-http-client -->
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client</artifactId>
|
||||
<version>1.23.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.http-client/google-http-client-gson -->
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client-gson</artifactId>
|
||||
<version>1.23.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>30.1.1-jre</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.mock-server/mockserver-netty -->
|
||||
<dependency>
|
||||
<groupId>org.mock-server</groupId>
|
||||
<artifactId>mockserver-netty</artifactId>
|
||||
<version>5.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>3.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk -->
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk</artifactId>
|
||||
<version>1.11.274</version>
|
||||
</dependency>
|
||||
<!-- API, java.xml.bind module -->
|
||||
<dependency>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Runtime, com.sun.xml.bind module -->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.5</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.10.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-compress -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.20</version>
|
||||
</dependency>
|
||||
<!-- prometheus metrics -->
|
||||
<dependency>
|
||||
<groupId>io.prometheus</groupId>
|
||||
<artifactId>simpleclient</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</dependency>
|
||||
<!-- Hotspot JVM metrics -->
|
||||
<dependency>
|
||||
<groupId>io.prometheus</groupId>
|
||||
<artifactId>simpleclient_hotspot</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</dependency>
|
||||
<!-- Expose metrics via a servlet -->
|
||||
<dependency>
|
||||
<groupId>io.prometheus</groupId>
|
||||
<artifactId>simpleclient_servlet</artifactId>
|
||||
<version>0.10.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,49 @@
|
|||
package uk.ac.ic.wlgitbridge;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.application.GitBridgeApp;
|
||||
import uk.ac.ic.wlgitbridge.bridge.Bridge;
|
||||
import uk.ac.ic.wlgitbridge.server.GitBridgeServer;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by Winston on 01/11/14.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is the entry point into the Git Bridge.
|
||||
*
|
||||
* It is responsible for creating the {@link GitBridgeApp} and then running it.
|
||||
*
|
||||
* The {@link GitBridgeApp} parses args and creates the {@link GitBridgeServer}.
|
||||
*
|
||||
* The {@link GitBridgeServer} creates the {@link Bridge}, among other things.
|
||||
*
|
||||
* The {@link Bridge} is the heart of the Git Bridge. Start there, and follow
|
||||
* the links outwards (which lead back to the Git users and the postback from
|
||||
* the snapshot API) and inwards (which lead into the components of the Git
|
||||
* Bridge: the configurable repo store, db store, and swap store, along with
|
||||
* the project lock, the swap job, the snapshot API, the resource cache
|
||||
* and the postback manager).
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Log.info(
|
||||
"Git Bridge started with args: "
|
||||
+ Arrays.toString(args)
|
||||
);
|
||||
try {
|
||||
new GitBridgeApp(args).run();
|
||||
} catch (Throwable t) {
|
||||
/* So that we get a timestamp */
|
||||
Log.error(
|
||||
"Fatal exception thrown to top level, exiting: ",
|
||||
t
|
||||
);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package uk.ac.ic.wlgitbridge.application;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.application.config.Config;
|
||||
import uk.ac.ic.wlgitbridge.application.exception.ArgsException;
|
||||
import uk.ac.ic.wlgitbridge.application.exception.ConfigFileException;
|
||||
import uk.ac.ic.wlgitbridge.server.GitBridgeServer;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 02/11/14.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class that represents the application. Parses arguments and gives them to the
|
||||
* server, or dies with a usage message.
|
||||
*/
|
||||
public class GitBridgeApp implements Runnable {
|
||||
|
||||
public static final int EXIT_CODE_FAILED = 1;
|
||||
private static final String USAGE_MESSAGE =
|
||||
"usage: writelatex-git-bridge [config_file]";
|
||||
|
||||
private String configFilePath;
|
||||
Config config;
|
||||
private GitBridgeServer server;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the WriteLatex-Git Bridge application.
|
||||
* @param args args from main, which should be in the format [config_file]
|
||||
*/
|
||||
public GitBridgeApp(String[] args) {
|
||||
try {
|
||||
parseArguments(args);
|
||||
loadConfigFile();
|
||||
Log.info("Config loaded: {}", config.getSanitisedString());
|
||||
} catch (ArgsException e) {
|
||||
printUsage();
|
||||
System.exit(EXIT_CODE_FAILED);
|
||||
} catch (ConfigFileException e) {
|
||||
Log.error(
|
||||
"The property for " +
|
||||
e.getMissingMember() +
|
||||
" is invalid. Check your config file."
|
||||
);
|
||||
System.exit(EXIT_CODE_FAILED);
|
||||
} catch (IOException e) {
|
||||
Log.error("Invalid config file. Check the file path.");
|
||||
System.exit(EXIT_CODE_FAILED);
|
||||
}
|
||||
try {
|
||||
server = new GitBridgeServer(config);
|
||||
} catch (ServletException e) {
|
||||
Log.error(
|
||||
"Servlet exception when instantiating GitBridgeServer",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server with the port number and root directory path given in
|
||||
* the command-line arguments.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
server.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
/* Helper methods */
|
||||
|
||||
private void parseArguments(String[] args) throws ArgsException {
|
||||
checkArgumentsLength(args);
|
||||
parseConfigFilePath(args);
|
||||
}
|
||||
|
||||
private void checkArgumentsLength(String[] args) throws ArgsException {
|
||||
if (args.length < 1) {
|
||||
throw new ArgsException();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseConfigFilePath(String[] args) throws ArgsException {
|
||||
configFilePath = args[0];
|
||||
}
|
||||
|
||||
private void loadConfigFile() throws ConfigFileException, IOException {
|
||||
Log.info("Loading config file at path: " + configFilePath);
|
||||
config = new Config(configFilePath);
|
||||
}
|
||||
|
||||
private void printUsage() {
|
||||
System.err.println(USAGE_MESSAGE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package uk.ac.ic.wlgitbridge.application.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import uk.ac.ic.wlgitbridge.application.exception.ConfigFileException;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStoreConfig;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.job.SwapJobConfig;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.store.SwapStoreConfig;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.base.JSONSource;
|
||||
import uk.ac.ic.wlgitbridge.util.Instance;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by Winston on 05/12/14.
|
||||
*/
|
||||
public class Config implements JSONSource {
|
||||
|
||||
static Config asSanitised(Config config) {
|
||||
return new Config(
|
||||
config.port,
|
||||
config.bindIp,
|
||||
config.idleTimeout,
|
||||
config.rootGitDirectory,
|
||||
config.apiBaseURL,
|
||||
config.postbackURL,
|
||||
config.serviceName,
|
||||
Oauth2.asSanitised(config.oauth2),
|
||||
config.repoStore,
|
||||
SwapStoreConfig.sanitisedCopy(config.swapStore),
|
||||
config.swapJob,
|
||||
config.sqliteHeapLimitBytes
|
||||
);
|
||||
}
|
||||
|
||||
private int port;
|
||||
private String bindIp;
|
||||
private int idleTimeout;
|
||||
private String rootGitDirectory;
|
||||
private String apiBaseURL;
|
||||
private String postbackURL;
|
||||
private String serviceName;
|
||||
@Nullable
|
||||
private Oauth2 oauth2;
|
||||
@Nullable
|
||||
private RepoStoreConfig repoStore;
|
||||
@Nullable
|
||||
private SwapStoreConfig swapStore;
|
||||
@Nullable
|
||||
private SwapJobConfig swapJob;
|
||||
private int sqliteHeapLimitBytes = 0;
|
||||
|
||||
public Config(
|
||||
String configFilePath
|
||||
) throws ConfigFileException,
|
||||
IOException {
|
||||
this(new FileReader(configFilePath));
|
||||
}
|
||||
|
||||
Config(Reader reader) {
|
||||
fromJSON(new Gson().fromJson(reader, JsonElement.class));
|
||||
}
|
||||
|
||||
public Config(
|
||||
int port,
|
||||
String bindIp,
|
||||
int idleTimeout,
|
||||
String rootGitDirectory,
|
||||
String apiBaseURL,
|
||||
String postbackURL,
|
||||
String serviceName,
|
||||
Oauth2 oauth2,
|
||||
RepoStoreConfig repoStore,
|
||||
SwapStoreConfig swapStore,
|
||||
SwapJobConfig swapJob,
|
||||
int sqliteHeapLimitBytes
|
||||
) {
|
||||
this.port = port;
|
||||
this.bindIp = bindIp;
|
||||
this.idleTimeout = idleTimeout;
|
||||
this.rootGitDirectory = rootGitDirectory;
|
||||
this.apiBaseURL = apiBaseURL;
|
||||
this.postbackURL = postbackURL;
|
||||
this.serviceName = serviceName;
|
||||
this.oauth2 = oauth2;
|
||||
this.repoStore = repoStore;
|
||||
this.swapStore = swapStore;
|
||||
this.swapJob = swapJob;
|
||||
this.sqliteHeapLimitBytes = sqliteHeapLimitBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromJSON(JsonElement json) {
|
||||
JsonObject configObject = json.getAsJsonObject();
|
||||
port = getElement(configObject, "port").getAsInt();
|
||||
bindIp = getElement(configObject, "bindIp").getAsString();
|
||||
idleTimeout = getElement(configObject, "idleTimeout").getAsInt();
|
||||
rootGitDirectory = getElement(
|
||||
configObject,
|
||||
"rootGitDirectory"
|
||||
).getAsString();
|
||||
String apiBaseURL = getElement(
|
||||
configObject,
|
||||
"apiBaseUrl"
|
||||
).getAsString();
|
||||
if (!apiBaseURL.endsWith("/")) {
|
||||
apiBaseURL += "/";
|
||||
}
|
||||
this.apiBaseURL = apiBaseURL;
|
||||
serviceName = getElement(configObject, "serviceName").getAsString();
|
||||
postbackURL = getElement(configObject, "postbackBaseUrl").getAsString();
|
||||
if (!postbackURL.endsWith("/")) {
|
||||
postbackURL += "/";
|
||||
}
|
||||
oauth2 = new Gson().fromJson(configObject.get("oauth2"), Oauth2.class);
|
||||
repoStore = new Gson().fromJson(
|
||||
configObject.get("repoStore"), RepoStoreConfig.class);
|
||||
swapStore = new Gson().fromJson(
|
||||
configObject.get("swapStore"),
|
||||
SwapStoreConfig.class
|
||||
);
|
||||
swapJob = new Gson().fromJson(
|
||||
configObject.get("swapJob"),
|
||||
SwapJobConfig.class
|
||||
);
|
||||
if (configObject.has("sqliteHeapLimitBytes")) {
|
||||
sqliteHeapLimitBytes = getElement(configObject, "sqliteHeapLimitBytes").getAsInt();
|
||||
}
|
||||
}
|
||||
|
||||
public String getSanitisedString() {
|
||||
return Instance.prettyGson.toJson(Config.asSanitised(this));
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getBindIp() {
|
||||
return bindIp;
|
||||
}
|
||||
|
||||
public int getIdleTimeout() {
|
||||
return idleTimeout;
|
||||
}
|
||||
|
||||
public String getRootGitDirectory() {
|
||||
return rootGitDirectory;
|
||||
}
|
||||
|
||||
public int getSqliteHeapLimitBytes() {
|
||||
return this.sqliteHeapLimitBytes;
|
||||
}
|
||||
|
||||
public String getAPIBaseURL() {
|
||||
return apiBaseURL;
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public String getPostbackURL() {
|
||||
return postbackURL;
|
||||
}
|
||||
|
||||
public boolean isUsingOauth2() {
|
||||
return oauth2 != null;
|
||||
}
|
||||
|
||||
public Oauth2 getOauth2() {
|
||||
if (!isUsingOauth2()) {
|
||||
throw new AssertionError("Getting oauth2 when not using it");
|
||||
}
|
||||
return oauth2;
|
||||
}
|
||||
|
||||
public Optional<RepoStoreConfig> getRepoStore() {
|
||||
return Optional.ofNullable(repoStore);
|
||||
}
|
||||
|
||||
public Optional<SwapStoreConfig> getSwapStore() {
|
||||
return Optional.ofNullable(swapStore);
|
||||
}
|
||||
|
||||
public Optional<SwapJobConfig> getSwapJob() {
|
||||
return Optional.ofNullable(swapJob);
|
||||
}
|
||||
|
||||
private JsonElement getElement(JsonObject configObject, String name) {
|
||||
JsonElement element = configObject.get(name);
|
||||
if (element == null) {
|
||||
throw new RuntimeException(new ConfigFileException(name));
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
private String getOptionalString(JsonObject configObject, String name) {
|
||||
JsonElement element = configObject.get(name);
|
||||
if (element == null || !element.isJsonPrimitive()) {
|
||||
return "";
|
||||
}
|
||||
return element.getAsString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package uk.ac.ic.wlgitbridge.application.config;
|
||||
|
||||
/**
|
||||
* Created by winston on 25/10/15.
|
||||
*/
|
||||
public class Oauth2 {
|
||||
|
||||
private final String oauth2ClientID;
|
||||
private final String oauth2ClientSecret;
|
||||
private final String oauth2Server;
|
||||
|
||||
public Oauth2(
|
||||
String oauth2ClientID,
|
||||
String oauth2ClientSecret,
|
||||
String oauth2Server
|
||||
) {
|
||||
this.oauth2ClientID = oauth2ClientID;
|
||||
this.oauth2ClientSecret = oauth2ClientSecret;
|
||||
this.oauth2Server = oauth2Server;
|
||||
}
|
||||
|
||||
public String getOauth2ClientID() {
|
||||
return oauth2ClientID;
|
||||
}
|
||||
|
||||
public String getOauth2ClientSecret() {
|
||||
return oauth2ClientSecret;
|
||||
}
|
||||
|
||||
public String getOauth2Server() {
|
||||
return oauth2Server;
|
||||
}
|
||||
|
||||
public static Oauth2 asSanitised(Oauth2 oauth2) {
|
||||
return new Oauth2(
|
||||
"<oauth2ClientID>",
|
||||
"<oauth2ClientSecret>",
|
||||
oauth2.oauth2Server
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package uk.ac.ic.wlgitbridge.application.exception;
|
||||
|
||||
/**
|
||||
* Created by Winston on 03/11/14.
|
||||
*/
|
||||
public class ArgsException extends Exception {}
|
|
@ -0,0 +1,18 @@
|
|||
package uk.ac.ic.wlgitbridge.application.exception;
|
||||
|
||||
/**
|
||||
* Created by Winston on 05/12/14.
|
||||
*/
|
||||
public class ConfigFileException extends Exception {
|
||||
|
||||
private final String missingMember;
|
||||
|
||||
public ConfigFileException(String missingMember) {
|
||||
this.missingMember = missingMember;
|
||||
}
|
||||
|
||||
public String getMissingMember() {
|
||||
return missingMember;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package uk.ac.ic.wlgitbridge.application.jetty;
|
||||
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Created by Winston on 03/11/14.
|
||||
*/
|
||||
public class NullLogger implements Logger {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "null_logger";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String s, Object... objects) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String s, Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String s, Object... objects) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String s, Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDebugEnabled(boolean b) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String s, Object... objects) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String s, long l) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String s, Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger(String s) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ignore(Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,839 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import uk.ac.ic.wlgitbridge.application.config.Config;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.ProjectState;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SqliteDBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.gc.GcJob;
|
||||
import uk.ac.ic.wlgitbridge.bridge.gc.GcJobImpl;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.LockGuard;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.resource.ResourceCache;
|
||||
import uk.ac.ic.wlgitbridge.bridge.resource.UrlResourceCache;
|
||||
import uk.ac.ic.wlgitbridge.bridge.snapshot.NetSnapshotApi;
|
||||
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi;
|
||||
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApiFacade;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.job.SwapJob;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.job.SwapJobImpl;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.store.S3SwapStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.store.SwapStore;
|
||||
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
||||
import uk.ac.ic.wlgitbridge.data.ProjectLockImpl;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawFile;
|
||||
import uk.ac.ic.wlgitbridge.data.model.Snapshot;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.FileLimitExceededException;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.WLReceivePackFactory;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.WLRepositoryResolver;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.WLUploadPackFactory;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook;
|
||||
import uk.ac.ic.wlgitbridge.server.FileHandler;
|
||||
import uk.ac.ic.wlgitbridge.server.PostbackContents;
|
||||
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.getdoc.GetDocResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PostbackManager;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PostbackPromise;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PushResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.*;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This is the heart of the Git Bridge. You plug in all the parts (project
|
||||
* lock, repo store, db store, swap store, snapshot api, resource cache and
|
||||
* postback manager) is called by Git user requests and Overleaf postback
|
||||
* requests.
|
||||
*
|
||||
* Follow these links to go "outward" (to input from Git users and Overleaf):
|
||||
*
|
||||
* 1. JGit hooks, which handle user Git requests:
|
||||
*
|
||||
* @see WLRepositoryResolver - used on all requests associate a repo with a
|
||||
* project name, or fail
|
||||
*
|
||||
* @see WLUploadPackFactory - used to handle clones and fetches
|
||||
*
|
||||
* @see WLReceivePackFactory - used to handle pushes by setting a hook
|
||||
* @see WriteLatexPutHook - the hook used to handle pushes
|
||||
*
|
||||
* 2. The Postback Servlet, which handles postbacks from the Overleaf app
|
||||
* to confirm that a project is pushed. If a postback is lost, it's fine, we
|
||||
* just update ourselves on the next access.
|
||||
*
|
||||
* @see PostbackHandler - the entry point for postbacks
|
||||
*
|
||||
* Follow these links to go "inward" (to the Git Bridge components):
|
||||
*
|
||||
* 1. The Project Lock, used to synchronise accesses to projects and shutdown
|
||||
* the Git Bridge gracefully by preventing further lock acquiring.
|
||||
*
|
||||
* @see ProjectLock - the interface used for the Project Lock
|
||||
* @see ProjectLockImpl - the default concrete implementation
|
||||
*
|
||||
* 2. The Repo Store, used to provide repository objects.
|
||||
*
|
||||
* The default implementation uses Git on the file system.
|
||||
*
|
||||
* @see RepoStore - the interface for the Repo Store
|
||||
* @see FSGitRepoStore - the default concrete implementation
|
||||
* @see ProjectRepo - an interface for an actual repo instance
|
||||
* @see GitProjectRepo - the default concrete implementation
|
||||
*
|
||||
* 3. The DB Store, used to store persistent data such as the latest version
|
||||
* of each project that we have (used for querying the Snapshot API), along
|
||||
* with caching remote blobs.
|
||||
*
|
||||
* The default implementation is SQLite based.
|
||||
*
|
||||
* @see DBStore - the interface for the DB store
|
||||
* @see SqliteDBStore - the default concrete implementation
|
||||
*
|
||||
* 4. The Swap Store, used to swap projects to when the disk goes over a
|
||||
* certain data usage.
|
||||
*
|
||||
* The default implementation tarbzips projects to/from Amazon S3.
|
||||
*
|
||||
* @see SwapStore - the interface for the Swap Store
|
||||
* @see S3SwapStore - the default concrete implementation
|
||||
*
|
||||
* 5. The Swap Job, which performs the actual swapping on the swap store based
|
||||
* on various configuration options.
|
||||
*
|
||||
* @see SwapJob - the interface for the Swap Job
|
||||
* @see SwapJobImpl - the default concrete implementation
|
||||
*
|
||||
* 6. The Snapshot API, which provides data from the Overleaf app.
|
||||
*
|
||||
* @see SnapshotApiFacade - wraps a concrete instance of the Snapshot API.
|
||||
* @see SnapshotApi - the interface for the Snapshot API.
|
||||
* @see NetSnapshotApi - the default concrete implementation
|
||||
*
|
||||
* 7. The Resource Cache, which provides the data for attachment resources from
|
||||
* URLs. It will generally fetch from the source on a cache miss.
|
||||
*
|
||||
* The default implementation uses the DB Store to maintain a mapping from
|
||||
* URLs to files in an actual repo.
|
||||
*
|
||||
* @see ResourceCache - the interface for the Resource Cache
|
||||
* @see UrlResourceCache - the default concrete implementation
|
||||
*
|
||||
* 8. The Postback Manager, which keeps track of pending postbacks. It stores a
|
||||
* mapping from project names to postback promises.
|
||||
*
|
||||
* @see PostbackManager - the class
|
||||
* @see PostbackPromise - the object waited on for a postback.
|
||||
*
|
||||
*/
|
||||
public class Bridge {
|
||||
|
||||
private final Config config;
|
||||
|
||||
private final ProjectLock lock;
|
||||
|
||||
private final RepoStore repoStore;
|
||||
private final DBStore dbStore;
|
||||
private final SwapStore swapStore;
|
||||
private final SwapJob swapJob;
|
||||
private final GcJob gcJob;
|
||||
|
||||
private final SnapshotApiFacade snapshotAPI;
|
||||
private final ResourceCache resourceCache;
|
||||
|
||||
private final PostbackManager postbackManager;
|
||||
|
||||
/**
|
||||
* Creates a Bridge from its configurable parts, which are the repo, db and
|
||||
* swap store, and the swap job config.
|
||||
*
|
||||
* This should be the method used to create a Bridge.
|
||||
* @param config The config to use
|
||||
* @param repoStore The repo store to use
|
||||
* @param dbStore The db store to use
|
||||
* @param swapStore The swap store to use
|
||||
* @param snapshotApi The snapshot api to use
|
||||
* @return The constructed Bridge.
|
||||
*/
|
||||
public static Bridge make(
|
||||
Config config,
|
||||
RepoStore repoStore,
|
||||
DBStore dbStore,
|
||||
SwapStore swapStore,
|
||||
SnapshotApi snapshotApi
|
||||
) {
|
||||
ProjectLock lock = new ProjectLockImpl((int threads) ->
|
||||
Log.info("Waiting for " + threads + " projects...")
|
||||
);
|
||||
return new Bridge(
|
||||
config,
|
||||
lock,
|
||||
repoStore,
|
||||
dbStore,
|
||||
swapStore,
|
||||
SwapJob.fromConfig(
|
||||
config.getSwapJob(),
|
||||
lock,
|
||||
repoStore,
|
||||
dbStore,
|
||||
swapStore
|
||||
),
|
||||
new GcJobImpl(repoStore, lock),
|
||||
new SnapshotApiFacade(snapshotApi),
|
||||
new UrlResourceCache(dbStore)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bridge from all of its components, not just its configurable
|
||||
* parts. This is for substituting mock/stub components for testing.
|
||||
* It's also used by Bridge.make to actually construct the bridge.
|
||||
* @param lock the {@link ProjectLock} to use
|
||||
* @param repoStore the {@link RepoStore} to use
|
||||
* @param dbStore the {@link DBStore} to use
|
||||
* @param swapStore the {@link SwapStore} to use
|
||||
* @param swapJob the {@link SwapJob} to use
|
||||
* @param gcJob
|
||||
* @param snapshotAPI the {@link SnapshotApi} to use
|
||||
* @param resourceCache the {@link ResourceCache} to use
|
||||
*/
|
||||
Bridge(
|
||||
Config config,
|
||||
ProjectLock lock,
|
||||
RepoStore repoStore,
|
||||
DBStore dbStore,
|
||||
SwapStore swapStore,
|
||||
SwapJob swapJob,
|
||||
GcJob gcJob,
|
||||
SnapshotApiFacade snapshotAPI,
|
||||
ResourceCache resourceCache
|
||||
) {
|
||||
this.config = config;
|
||||
this.lock = lock;
|
||||
this.repoStore = repoStore;
|
||||
this.dbStore = dbStore;
|
||||
this.swapStore = swapStore;
|
||||
this.snapshotAPI = snapshotAPI;
|
||||
this.resourceCache = resourceCache;
|
||||
this.swapJob = swapJob;
|
||||
this.gcJob = gcJob;
|
||||
postbackManager = new PostbackManager();
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::doShutdown));
|
||||
repoStore.purgeNonexistentProjects(dbStore.getProjectNames());
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs the graceful shutdown of the Bridge, which is called by the
|
||||
* shutdown hook. It acquires the project write lock, which prevents
|
||||
* work being done for new projects (which acquire the read lock).
|
||||
* Once it has the write lock, there are no readers left, so the git bridge
|
||||
* can shut down gracefully.
|
||||
*
|
||||
* It is also used by the tests.
|
||||
*/
|
||||
void doShutdown() {
|
||||
Log.info("Shutdown received.");
|
||||
Log.info("Stopping SwapJob");
|
||||
swapJob.stop();
|
||||
Log.info("Stopping GcJob");
|
||||
gcJob.stop();
|
||||
Log.info("Waiting for projects");
|
||||
lock.lockAll();
|
||||
Log.info("Bye");
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the swap job, which will begin checking whether projects should be
|
||||
* swapped with a configurable frequency.
|
||||
*/
|
||||
public void startBackgroundJobs() {
|
||||
swapJob.start();
|
||||
gcJob.start();
|
||||
}
|
||||
|
||||
public boolean healthCheck() {
|
||||
try {
|
||||
dbStore.getNumProjects();
|
||||
File rootDirectory = new File("/");
|
||||
if (!rootDirectory.exists()) {
|
||||
throw new Exception("bad filesystem state, root directory does not exist");
|
||||
}
|
||||
Log.info("[HealthCheck] passed");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[HealthCheck] FAILED!", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a check of inconsistencies in the DB. This was used to upgrade
|
||||
* the schema.
|
||||
*/
|
||||
public void checkDB() {
|
||||
Log.info("Checking DB");
|
||||
File rootDir = repoStore.getRootDirectory();
|
||||
for (File f : rootDir.listFiles()) {
|
||||
if (f.getName().equals(".wlgb")) {
|
||||
continue;
|
||||
}
|
||||
String projName = f.getName();
|
||||
try (LockGuard __ = lock.lockGuard(projName)) {
|
||||
File dotGit = new File(f, ".git");
|
||||
if (!dotGit.exists()) {
|
||||
Log.warn("Project: {} has no .git", projName);
|
||||
continue;
|
||||
}
|
||||
ProjectState state = dbStore.getProjectState(projName);
|
||||
if (state != ProjectState.NOT_PRESENT) {
|
||||
continue;
|
||||
}
|
||||
Log.warn(
|
||||
"Project: {} not in swap_store, adding",
|
||||
projName
|
||||
);
|
||||
dbStore.setLastAccessedTime(
|
||||
projName,
|
||||
new Timestamp(dotGit.lastModified())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronises the given repository with Overleaf.
|
||||
*
|
||||
* It acquires the project lock and calls
|
||||
* {@link #getUpdatedRepoCritical(Optional, String, GetDocResult)}.
|
||||
* @param oauth2 The oauth2 to use
|
||||
* @param projectName The name of the project
|
||||
* @throws IOException
|
||||
* @throws GitUserException
|
||||
*/
|
||||
public ProjectRepo getUpdatedRepo(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName
|
||||
) throws IOException, GitUserException {
|
||||
try (LockGuard __ = lock.lockGuard(projectName)) {
|
||||
Optional<GetDocResult> maybeDoc = snapshotAPI.getDoc(oauth2, projectName);
|
||||
if (!maybeDoc.isPresent()) {
|
||||
throw new RepositoryNotFoundException(projectName);
|
||||
}
|
||||
GetDocResult doc = maybeDoc.get();
|
||||
Log.info("[{}] Updating repository", projectName);
|
||||
return getUpdatedRepoCritical(oauth2, projectName, doc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronises the given repository with Overleaf.
|
||||
*
|
||||
* Pre: the project lock must be acquired for the given repo.
|
||||
*
|
||||
* 1. Queries the project state for the given project name.
|
||||
* a. NOT_PRESENT = We've never seen it before, and the row for the
|
||||
* project doesn't even exist. The project definitely
|
||||
* exists because we would have aborted otherwise.
|
||||
* b. PRESENT = The project is on disk.
|
||||
* c. SWAPPED = The project is in the {@link SwapStore}
|
||||
*
|
||||
* If the project has never been cloned, it is git init'd. If the project
|
||||
* is in swap, it is restored to disk. Otherwise, the project was already
|
||||
* present.
|
||||
*
|
||||
* With the project present, snapshots are downloaded from the snapshot
|
||||
* API with {@link #updateProject(Optional, ProjectRepo)}.
|
||||
*
|
||||
* Then, the last accessed time of the project is set to the current time.
|
||||
* This is to support the LRU of the swap store.
|
||||
* @param oauth2
|
||||
* @param projectName The name of the project
|
||||
* @throws IOException
|
||||
* @throws GitUserException
|
||||
*/
|
||||
private ProjectRepo getUpdatedRepoCritical(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
GetDocResult doc
|
||||
) throws IOException, GitUserException {
|
||||
ProjectRepo repo;
|
||||
ProjectState state = dbStore.getProjectState(projectName);
|
||||
switch (state) {
|
||||
case NOT_PRESENT:
|
||||
Log.info("[{}] Repo not present", projectName);
|
||||
String migratedFromID = doc.getMigratedFromID();
|
||||
if (migratedFromID != null) {
|
||||
Log.info("[{}] Has a migratedFromId: {}", projectName, migratedFromID);
|
||||
try (LockGuard __ = lock.lockGuard(migratedFromID)) {
|
||||
ProjectState sourceState = dbStore.getProjectState(migratedFromID);
|
||||
switch (sourceState) {
|
||||
case NOT_PRESENT:
|
||||
// Normal init-repo
|
||||
Log.info("[{}] migrated-from project not present, proceed as normal",
|
||||
projectName
|
||||
);
|
||||
repo = repoStore.initRepo(projectName);
|
||||
break;
|
||||
case SWAPPED:
|
||||
// Swap back and then copy
|
||||
swapJob.restore(migratedFromID);
|
||||
/* Fallthrough */
|
||||
default:
|
||||
// Copy data, and set version to zero
|
||||
Log.info("[{}] Init from other project: {}",
|
||||
projectName,
|
||||
migratedFromID
|
||||
);
|
||||
repo = repoStore.initRepoFromExisting(projectName, migratedFromID);
|
||||
dbStore.setLatestVersionForProject(migratedFromID, 0);
|
||||
dbStore.setLastAccessedTime(
|
||||
migratedFromID,
|
||||
Timestamp.valueOf(LocalDateTime.now())
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
repo = repoStore.initRepo(projectName);
|
||||
break;
|
||||
}
|
||||
case SWAPPED:
|
||||
swapJob.restore(projectName);
|
||||
/* Fallthrough */
|
||||
default:
|
||||
repo = repoStore.getExistingRepo(projectName);
|
||||
}
|
||||
updateProject(oauth2, repo);
|
||||
dbStore.setLastAccessedTime(
|
||||
projectName,
|
||||
Timestamp.valueOf(LocalDateTime.now())
|
||||
);
|
||||
return repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public call to push a project.
|
||||
*
|
||||
* It acquires the lock and calls {@link #pushCritical(
|
||||
* Optional,
|
||||
* String,
|
||||
* RawDirectory,
|
||||
* RawDirectory
|
||||
* )}, catching exceptions, logging, and rethrowing them.
|
||||
* @param oauth2 The oauth2 to use for the snapshot API
|
||||
* @param projectName The name of the project to push to
|
||||
* @param directoryContents The new contents of the project
|
||||
* @param oldDirectoryContents The old contents of the project
|
||||
* @param hostname
|
||||
* @throws SnapshotPostException
|
||||
* @throws IOException
|
||||
* @throws MissingRepositoryException
|
||||
* @throws ForbiddenException
|
||||
* @throws GitUserException
|
||||
*/
|
||||
public void push(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents,
|
||||
String hostname
|
||||
) throws SnapshotPostException, IOException, MissingRepositoryException, ForbiddenException, GitUserException {
|
||||
Log.debug("[{}] pushing to Overleaf", projectName);
|
||||
try (LockGuard __ = lock.lockGuard(projectName)) {
|
||||
pushCritical(
|
||||
oauth2,
|
||||
projectName,
|
||||
directoryContents,
|
||||
oldDirectoryContents
|
||||
);
|
||||
} catch (SevereSnapshotPostException e) {
|
||||
Log.warn(
|
||||
"[" + projectName + "] Failed to put to Overleaf",
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
} catch (SnapshotPostException e) {
|
||||
/* Stack trace should be printed further up */
|
||||
Log.warn(
|
||||
"[{}] Exception when waiting for postback: {}",
|
||||
projectName,
|
||||
e.getClass().getSimpleName()
|
||||
);
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
Log.warn("[{}] IOException on put: {}", projectName, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
gcJob.queueForGc(projectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the work of pushing to a project, assuming the project lock is held.
|
||||
* The {@link WriteLatexPutHook} is the original caller, and when we return
|
||||
* without throwing, the commit is committed.
|
||||
*
|
||||
* We start off by creating a postback key, which is given in the url when
|
||||
* the Overleaf app tries to access the atts.
|
||||
*
|
||||
* Then creates a {@link CandidateSnapshot} from the old and new project
|
||||
* contents. The
|
||||
* {@link CandidateSnapshot} is created using
|
||||
* {@link #createCandidateSnapshot(String, RawDirectory, RawDirectory)},
|
||||
* which creates the snapshot object and writes the push files to the
|
||||
* atts directory, which is served by the {@link PostbackHandler}.
|
||||
* The files are deleted at the end of a try-with-resources block.
|
||||
*
|
||||
* Then 3 things are used to make the push request to the snapshot API:
|
||||
* 1. The oauth2
|
||||
* 2. The candidate snapshot
|
||||
* 3. The postback key
|
||||
*
|
||||
* If the snapshot API reports this as not successful, we immediately throw
|
||||
* an {@link OutOfDateException}, which goes back to the user.
|
||||
*
|
||||
* Otherwise, we wait (with a timeout) on a promise from the postback
|
||||
* manager, which can throw back to the user.
|
||||
*
|
||||
* If this is successful, we approve the snapshot with
|
||||
* {@link #approveSnapshot(int, CandidateSnapshot)}, which updates our side
|
||||
* of the push: the latest version and the URL index store.
|
||||
*
|
||||
* Then, we set the last accessed time for the swap store.
|
||||
*
|
||||
* Finally, after we return, the push to the repo from the hook is
|
||||
* successful and the repo gets updated.
|
||||
*
|
||||
* @param oauth2
|
||||
* @param projectName
|
||||
* @param directoryContents
|
||||
* @param oldDirectoryContents
|
||||
* @throws IOException
|
||||
* @throws MissingRepositoryException
|
||||
* @throws ForbiddenException
|
||||
* @throws SnapshotPostException
|
||||
* @throws GitUserException
|
||||
*/
|
||||
private void pushCritical(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents
|
||||
) throws IOException, MissingRepositoryException, ForbiddenException, SnapshotPostException, GitUserException {
|
||||
Optional<Long> maxFileNum = config
|
||||
.getRepoStore()
|
||||
.flatMap(RepoStoreConfig::getMaxFileNum);
|
||||
if (maxFileNum.isPresent()) {
|
||||
long maxFileNum_ = maxFileNum.get();
|
||||
if (directoryContents.getFileTable().size() > maxFileNum_) {
|
||||
Log.debug("[{}] Too many files: {}/{}", projectName, directoryContents.getFileTable().size(), maxFileNum_);
|
||||
throw new FileLimitExceededException(directoryContents.getFileTable().size(), maxFileNum_);
|
||||
}
|
||||
}
|
||||
Log.info("[{}] Pushing files ({} new, {} old)", projectName, directoryContents.getFileTable().size(), oldDirectoryContents.getFileTable().size());
|
||||
String postbackKey = postbackManager.makeKeyForProject(projectName);
|
||||
Log.info(
|
||||
"[{}] Created postback key: {}",
|
||||
projectName,
|
||||
postbackKey
|
||||
);
|
||||
try (
|
||||
CandidateSnapshot candidate = createCandidateSnapshot(
|
||||
projectName,
|
||||
directoryContents,
|
||||
oldDirectoryContents
|
||||
);
|
||||
) {
|
||||
Log.info(
|
||||
"[{}] Candidate snapshot created: {}",
|
||||
projectName,
|
||||
candidate
|
||||
);
|
||||
PushResult result
|
||||
= snapshotAPI.push(oauth2, candidate, postbackKey);
|
||||
if (result.wasSuccessful()) {
|
||||
Log.info(
|
||||
"[{}] Push to Overleaf successful",
|
||||
projectName
|
||||
);
|
||||
Log.info("[{}] Waiting for postback...", projectName);
|
||||
int versionID =
|
||||
postbackManager.waitForVersionIdOrThrow(projectName);
|
||||
Log.info(
|
||||
"[{}] Got version ID for push: {}",
|
||||
projectName,
|
||||
versionID
|
||||
);
|
||||
approveSnapshot(versionID, candidate);
|
||||
Log.info(
|
||||
"[{}] Approved version ID: {}",
|
||||
projectName,
|
||||
versionID
|
||||
);
|
||||
dbStore.setLastAccessedTime(
|
||||
projectName,
|
||||
Timestamp.valueOf(LocalDateTime.now())
|
||||
);
|
||||
} else {
|
||||
Log.warn(
|
||||
"[{}] Went out of date while waiting for push",
|
||||
projectName
|
||||
);
|
||||
throw new OutOfDateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A public call that should originate from the {@link FileHandler}.
|
||||
*
|
||||
* The {@link FileHandler} serves atts to the Overleaf app during a push.
|
||||
* The Overleaf app includes the postback key in the request, which was
|
||||
* originally given on a push request.
|
||||
*
|
||||
* This method checks that the postback key matches, and throws if not.
|
||||
*
|
||||
* The FileHandler should not serve the file if this throws.
|
||||
* @param projectName The project name that this key belongs to
|
||||
* @param postbackKey The key
|
||||
* @throws InvalidPostbackKeyException If the key doesn't match
|
||||
*/
|
||||
public void checkPostbackKey(String projectName, String postbackKey)
|
||||
throws InvalidPostbackKeyException {
|
||||
postbackManager.checkPostbackKey(projectName, postbackKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* A public call that originates from the postback thread
|
||||
* {@link PostbackContents#processPostback()}, i.e. once the Overleaf app
|
||||
* has fetched all the atts and has committed the push and is happy, it
|
||||
* calls back here, fulfilling the promise that the push
|
||||
* {@link #push(Optional, String, RawDirectory, RawDirectory, String)}
|
||||
* is waiting on.
|
||||
*
|
||||
* The Overleaf app will have invented a new version for the push, which is
|
||||
* passed to the promise for the original push request to update the app.
|
||||
* @param projectName The name of the project being pushed to
|
||||
* @param postbackKey The postback key being used
|
||||
* @param versionID the new version id to use
|
||||
* @throws UnexpectedPostbackException if the postback key is invalid
|
||||
*/
|
||||
public void postbackReceivedSuccessfully(
|
||||
String projectName,
|
||||
String postbackKey,
|
||||
int versionID
|
||||
) throws UnexpectedPostbackException {
|
||||
Log.info(
|
||||
"[{}]" +
|
||||
" Postback received by postback thread, version: {}",
|
||||
projectName,
|
||||
versionID);
|
||||
postbackManager.postVersionIDForProject(
|
||||
projectName,
|
||||
versionID,
|
||||
postbackKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link #postbackReceivedSuccessfully(String, String, int)},
|
||||
* but with an exception instead.
|
||||
*
|
||||
* This is based on the JSON body of the postback from the Overleaf app.
|
||||
*
|
||||
* The most likely problem is an {@link OutOfDateException}.
|
||||
* @param projectName The name of the project
|
||||
* @param postbackKey The postback key being used
|
||||
* @param exception The exception encountered
|
||||
* @throws UnexpectedPostbackException If the postback key is invalid
|
||||
*/
|
||||
public void postbackReceivedWithException(
|
||||
String projectName,
|
||||
String postbackKey,
|
||||
SnapshotPostException exception
|
||||
) throws UnexpectedPostbackException {
|
||||
Log.warn("[{}] Postback received with exception", projectName);
|
||||
postbackManager.postExceptionForProject(
|
||||
projectName,
|
||||
exception,
|
||||
postbackKey
|
||||
);
|
||||
}
|
||||
|
||||
/* PRIVATE */
|
||||
|
||||
/**
|
||||
* Called by {@link #getUpdatedRepoCritical(Optional, String)}
|
||||
*
|
||||
* Does the actual work of getting the snapshots for a project from the
|
||||
* snapshot API and committing them to a repo.
|
||||
*
|
||||
* If any snapshots were found, sets the latest version for the project.
|
||||
*
|
||||
* @param oauth2
|
||||
* @param repo
|
||||
* @throws IOException
|
||||
* @throws GitUserException
|
||||
*/
|
||||
private void updateProject(
|
||||
Optional<Credential> oauth2,
|
||||
ProjectRepo repo
|
||||
) throws IOException, GitUserException {
|
||||
String projectName = repo.getProjectName();
|
||||
int latestVersionId = dbStore.getLatestVersionForProject(projectName);
|
||||
Deque<Snapshot> snapshots = snapshotAPI.getSnapshots(
|
||||
oauth2, projectName, latestVersionId);
|
||||
|
||||
makeCommitsFromSnapshots(repo, snapshots);
|
||||
|
||||
// TODO: in case crashes around here, add an
|
||||
// "updating_from_commit" column to the DB as a way to rollback the
|
||||
// any failed partial updates before re-trying
|
||||
// Also need to consider the empty state (a new git init'd repo being
|
||||
// the rollback target)
|
||||
if (!snapshots.isEmpty()) {
|
||||
dbStore.setLatestVersionForProject(
|
||||
projectName,
|
||||
snapshots.getLast().getVersionID()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link #updateProject(Optional, ProjectRepo)}.
|
||||
*
|
||||
* Performs the actual Git commits on the disk.
|
||||
*
|
||||
* Each commit adds files to the db store
|
||||
* ({@link ResourceCache#get(String, String, String, Map, Map, Optional)},
|
||||
* and then removes any files that were deleted.
|
||||
* @param repo The repository to commit to
|
||||
* @param snapshots The snapshots to commit
|
||||
* @throws IOException If an IOException occurred
|
||||
* @throws SizeLimitExceededException If one of the files was too big.
|
||||
*/
|
||||
private void makeCommitsFromSnapshots(
|
||||
ProjectRepo repo,
|
||||
Collection<Snapshot> snapshots
|
||||
) throws IOException, GitUserException {
|
||||
String name = repo.getProjectName();
|
||||
Optional<Long> maxSize = config
|
||||
.getRepoStore()
|
||||
.flatMap(RepoStoreConfig::getMaxFileSize);
|
||||
for (Snapshot snapshot : snapshots) {
|
||||
RawDirectory directory = repo.getDirectory();
|
||||
Map<String, RawFile> fileTable = directory.getFileTable();
|
||||
List<RawFile> files = new ArrayList<>();
|
||||
files.addAll(snapshot.getSrcs());
|
||||
for (RawFile file : files) {
|
||||
long size = file.size();
|
||||
/* Can't throw in ifPresent... */
|
||||
if (maxSize.isPresent()) {
|
||||
long maxSize_ = maxSize.get();
|
||||
if (size >= maxSize_) {
|
||||
throw new SizeLimitExceededException(
|
||||
Optional.of(file.getPath()), size, maxSize_);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, byte[]> fetchedUrls = new HashMap<>();
|
||||
for (SnapshotAttachment snapshotAttachment : snapshot.getAtts()) {
|
||||
files.add(
|
||||
resourceCache.get(
|
||||
name,
|
||||
snapshotAttachment.getUrl(),
|
||||
snapshotAttachment.getPath(),
|
||||
fileTable,
|
||||
fetchedUrls,
|
||||
maxSize
|
||||
)
|
||||
);
|
||||
}
|
||||
Log.info(
|
||||
"[{}] Committing version ID: {}",
|
||||
name,
|
||||
snapshot.getVersionID()
|
||||
);
|
||||
Collection<String> missingFiles = repo.commitAndGetMissing(
|
||||
new GitDirectoryContents(
|
||||
files,
|
||||
repoStore.getRootDirectory(),
|
||||
name,
|
||||
snapshot
|
||||
)
|
||||
);
|
||||
dbStore.deleteFilesForProject(
|
||||
name,
|
||||
missingFiles.toArray(new String[missingFiles.size()])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by
|
||||
* {@link #pushCritical(Optional, String, RawDirectory, RawDirectory)}.
|
||||
*
|
||||
* This call consists of 2 things: Creating the candidate snapshot,
|
||||
* and writing the atts to the atts directory.
|
||||
*
|
||||
* The candidate snapshot RAIIs away those atts (use try-with-resources).
|
||||
* @param projectName The name of the project
|
||||
* @param directoryContents The new directory contents
|
||||
* @param oldDirectoryContents The old directory contents
|
||||
* @return The {@link CandidateSnapshot} created
|
||||
* @throws IOException If an I/O exception occurred on writing
|
||||
*/
|
||||
private CandidateSnapshot createCandidateSnapshot(
|
||||
String projectName,
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents
|
||||
) throws IOException {
|
||||
CandidateSnapshot candidateSnapshot = new CandidateSnapshot(
|
||||
projectName,
|
||||
dbStore.getLatestVersionForProject(projectName),
|
||||
directoryContents,
|
||||
oldDirectoryContents
|
||||
);
|
||||
candidateSnapshot.writeServletFiles(repoStore.getRootDirectory());
|
||||
return candidateSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by
|
||||
* {@link #pushCritical(Optional, String, RawDirectory, RawDirectory)}.
|
||||
*
|
||||
* This method approves a push by setting the latest version and removing
|
||||
* any deleted files from the db store (files were already added by the
|
||||
* resources cache).
|
||||
* @param versionID
|
||||
* @param candidateSnapshot
|
||||
*/
|
||||
private void approveSnapshot(
|
||||
int versionID,
|
||||
CandidateSnapshot candidateSnapshot
|
||||
) {
|
||||
List<String> deleted = candidateSnapshot.getDeleted();
|
||||
dbStore.setLatestVersionForProject(
|
||||
candidateSnapshot.getProjectName(),
|
||||
versionID
|
||||
);
|
||||
dbStore.deleteFilesForProject(
|
||||
candidateSnapshot.getProjectName(),
|
||||
deleted.toArray(new String[deleted.size()])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class DBInitException extends RuntimeException {
|
||||
|
||||
public DBInitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DBInitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public DBInitException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface DBStore {
|
||||
|
||||
int getNumProjects();
|
||||
|
||||
List<String> getProjectNames();
|
||||
|
||||
void setLatestVersionForProject(String project, int versionID);
|
||||
|
||||
int getLatestVersionForProject(String project);
|
||||
|
||||
void addURLIndexForProject(String projectName, String url, String path);
|
||||
|
||||
void deleteFilesForProject(String project, String... files);
|
||||
|
||||
String getPathForURLInProject(String projectName, String url);
|
||||
|
||||
String getOldestUnswappedProject();
|
||||
|
||||
void swap(String projectName, String compressionMethod);
|
||||
|
||||
void restore(String projectName);
|
||||
|
||||
String getSwapCompression(String projectName);
|
||||
|
||||
int getNumUnswappedProjects();
|
||||
|
||||
ProjectState getProjectState(String projectName);
|
||||
|
||||
/**
|
||||
* Sets the last accessed time for the given project name.
|
||||
* @param projectName the project's name
|
||||
* @param time the time, or null if the project is to be swapped
|
||||
*/
|
||||
void setLastAccessedTime(String projectName, Timestamp time);
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public enum ProjectState {
|
||||
|
||||
NOT_PRESENT,
|
||||
PRESENT,
|
||||
SWAPPED
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.noop;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.ProjectState;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
|
||||
public class NoopDbStore implements DBStore {
|
||||
|
||||
@Override
|
||||
public int getNumProjects() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getProjectNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLatestVersionForProject(String project, int versionID) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLatestVersionForProject(String project) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addURLIndexForProject(String projectName, String url, String path) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFilesForProject(String project, String... files) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathForURLInProject(String projectName, String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOldestUnswappedProject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumUnswappedProjects() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectState getProjectState(String projectName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(String projectName, Timestamp time) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swap(String projectName, String compressionMethod) {}
|
||||
|
||||
@Override
|
||||
public void restore(String projectName) {}
|
||||
|
||||
@Override
|
||||
public String getSwapCompression(String projectName) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public interface SQLQuery<T> extends SQLUpdate {
|
||||
|
||||
public T processResultSet(ResultSet resultSet) throws SQLException;
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public interface SQLUpdate {
|
||||
|
||||
String getSQL();
|
||||
default void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBInitException;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.ProjectState;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.query.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.create.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.delete.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Created by Winston on 17/11/14.
|
||||
*/
|
||||
public class SqliteDBStore implements DBStore {
|
||||
|
||||
private final Connection connection;
|
||||
private int heapLimitBytes = 0;
|
||||
|
||||
public SqliteDBStore(File dbFile) {
|
||||
this(dbFile, 0);
|
||||
}
|
||||
|
||||
public SqliteDBStore(File dbFile, int heapLimitBytes) {
|
||||
this.heapLimitBytes = heapLimitBytes;
|
||||
try {
|
||||
connection = openConnectionTo(dbFile);
|
||||
createTables();
|
||||
} catch (Throwable t) {
|
||||
throw new DBInitException(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumProjects() {
|
||||
return query(new GetNumProjects());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getProjectNames() {
|
||||
return query(new GetProjectNamesSQLQuery());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLatestVersionForProject(
|
||||
String projectName,
|
||||
int versionID
|
||||
) {
|
||||
update(new SetProjectSQLUpdate(projectName, versionID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLatestVersionForProject(
|
||||
String projectName
|
||||
) {
|
||||
return query(new GetLatestVersionForProjectSQLQuery(projectName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addURLIndexForProject(
|
||||
String projectName,
|
||||
String url,
|
||||
String path
|
||||
) {
|
||||
update(new AddURLIndexSQLUpdate(projectName, url, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFilesForProject(
|
||||
String projectName,
|
||||
String... paths
|
||||
) {
|
||||
update(new DeleteFilesForProjectSQLUpdate(projectName, paths));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathForURLInProject(
|
||||
String projectName,
|
||||
String url
|
||||
) {
|
||||
return query(new GetPathForURLInProjectSQLQuery(projectName, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOldestUnswappedProject() {
|
||||
return query(new GetOldestProjectName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumUnswappedProjects() {
|
||||
return query(new GetNumUnswappedProjects());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectState getProjectState(String projectName) {
|
||||
return query(new GetProjectState(projectName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(
|
||||
String projectName,
|
||||
Timestamp lastAccessed
|
||||
) {
|
||||
update(new SetProjectLastAccessedTime(projectName, lastAccessed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swap(String projectName, String compressionMethod) {
|
||||
update(new UpdateSwap(projectName, compressionMethod));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restore(String projectName) {
|
||||
update(new UpdateRestore(projectName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSwapCompression(String projectName) {
|
||||
return query(new GetSwapCompression(projectName));
|
||||
}
|
||||
|
||||
private Connection openConnectionTo(File dbFile) {
|
||||
File parentDir = dbFile.getParentFile();
|
||||
if (!parentDir.exists() && !parentDir.mkdirs()) {
|
||||
throw new DBInitException(
|
||||
parentDir.getAbsolutePath() + " directory didn't exist, " +
|
||||
"and unable to create. Check your permissions."
|
||||
);
|
||||
}
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new DBInitException(e);
|
||||
}
|
||||
try {
|
||||
return DriverManager.getConnection(
|
||||
"jdbc:sqlite:" + dbFile.getAbsolutePath()
|
||||
);
|
||||
} catch (SQLException e) {
|
||||
throw new DBInitException("Unable to connect to DB", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTables() {
|
||||
/* Migrations */
|
||||
/* We need to eat exceptions from here */
|
||||
try { doUpdate(new SetSoftHeapLimitPragma(this.heapLimitBytes)); } catch (SQLException ignore) {}
|
||||
try { doUpdate(new ProjectsAddLastAccessed()); } catch (SQLException ignore) {}
|
||||
try { doUpdate(new ProjectsAddSwapTime()); } catch (SQLException ignore) {}
|
||||
try { doUpdate(new ProjectsAddRestoreTime()); } catch (SQLException ignore) {}
|
||||
try { doUpdate(new ProjectsAddSwapCompression()); } catch (SQLException ignore) {}
|
||||
|
||||
/* Create tables (if they don't exist) */
|
||||
Stream.of(
|
||||
new CreateProjectsTableSQLUpdate(),
|
||||
new CreateProjectsIndexLastAccessed(),
|
||||
new CreateURLIndexStoreSQLUpdate(),
|
||||
new CreateIndexURLIndexStore()
|
||||
).forEach(this::update);
|
||||
|
||||
/* In the case of needing to change the schema, we need to check that
|
||||
migrations didn't just fail */
|
||||
Preconditions.checkState(query(new LastAccessedColumnExists()));
|
||||
Preconditions.checkState(query(new SwapTimeColumnExists()));
|
||||
Preconditions.checkState(query(new RestoreTimeColumnExists()));
|
||||
Preconditions.checkState(query(new SwapCompressionColumnExists()));
|
||||
}
|
||||
|
||||
private void update(SQLUpdate update) {
|
||||
try {
|
||||
doUpdate(update);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T query(SQLQuery<T> query) {
|
||||
try {
|
||||
return doQuery(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void doUpdate(SQLUpdate update) throws SQLException {
|
||||
PreparedStatement statement = null;
|
||||
try {
|
||||
statement = connection.prepareStatement(update.getSQL());
|
||||
update.addParametersToStatement(statement);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
statement.close();
|
||||
} catch (Throwable t) {
|
||||
throw new SQLException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T doQuery(SQLQuery<T> query) throws SQLException {
|
||||
PreparedStatement statement = null;
|
||||
ResultSet results = null;
|
||||
try {
|
||||
statement = connection.prepareStatement(query.getSQL());
|
||||
query.addParametersToStatement(statement);
|
||||
results = statement.executeQuery();
|
||||
return query.processResultSet(results);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} finally {
|
||||
if (statement != null) {
|
||||
statement.close();
|
||||
}
|
||||
if (results != null) {
|
||||
results.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class GetLatestVersionForProjectSQLQuery implements SQLQuery<Integer> {
|
||||
|
||||
private static final String GET_VERSION_IDS_FOR_PROJECT_NAME =
|
||||
"SELECT `version_id` FROM `projects` WHERE `name` = ?";
|
||||
|
||||
private final String projectName;
|
||||
|
||||
public GetLatestVersionForProjectSQLQuery(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer processResultSet(ResultSet resultSet) throws SQLException {
|
||||
int versionID = 0;
|
||||
while (resultSet.next()) {
|
||||
versionID = resultSet.getInt("version_id");
|
||||
}
|
||||
return versionID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_VERSION_IDS_FOR_PROJECT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class GetNumProjects implements SQLQuery<Integer> {
|
||||
|
||||
private static final String GET_NUM_PROJECTS =
|
||||
"SELECT COUNT(*)\n" +
|
||||
" FROM `projects`";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_NUM_PROJECTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
return resultSet.getInt("COUNT(*)");
|
||||
}
|
||||
throw new IllegalStateException("Count always returns results");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class GetNumUnswappedProjects implements SQLQuery<Integer> {
|
||||
|
||||
private static final String GET_NUM_UNSWAPPED_PROJECTS =
|
||||
"SELECT COUNT(*)\n" +
|
||||
" FROM `projects`\n" +
|
||||
" WHERE `last_accessed` IS NOT NULL";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_NUM_UNSWAPPED_PROJECTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
return resultSet.getInt("COUNT(*)");
|
||||
}
|
||||
throw new IllegalStateException("Count always returns results");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class GetOldestProjectName implements SQLQuery<String> {
|
||||
|
||||
private static final String GET_OLDEST_PROJECT_NAME =
|
||||
"SELECT `name`, MIN(`last_accessed`)\n" +
|
||||
" FROM `projects` \n" +
|
||||
" WHERE `last_accessed` IS NOT NULL;";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_OLDEST_PROJECT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
return resultSet.getString("name");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class GetPathForURLInProjectSQLQuery implements SQLQuery<String> {
|
||||
|
||||
private static final String GET_URL_INDEXES_FOR_PROJECT_NAME =
|
||||
"SELECT `path` "
|
||||
+ "FROM `url_index_store` "
|
||||
+ "WHERE `project_name` = ? "
|
||||
+ "AND `url` = ?";
|
||||
|
||||
private final String projectName;
|
||||
private final String url;
|
||||
|
||||
public GetPathForURLInProjectSQLQuery(String projectName, String url) {
|
||||
this.projectName = projectName;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processResultSet(ResultSet resultSet) throws SQLException {
|
||||
String path = null;
|
||||
while (resultSet.next()) {
|
||||
path = resultSet.getString("path");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_URL_INDEXES_FOR_PROJECT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
statement.setString(2, url);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Winston on 21/02/15.
|
||||
*/
|
||||
public class GetProjectNamesSQLQuery implements SQLQuery<List<String>> {
|
||||
|
||||
private static final String GET_URL_INDEXES_FOR_PROJECT_NAME =
|
||||
"SELECT `name` FROM `projects`";
|
||||
|
||||
@Override
|
||||
public List<String> processResultSet(
|
||||
ResultSet resultSet
|
||||
) throws SQLException {
|
||||
List<String> projectNames = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
projectNames.add(resultSet.getString("name"));
|
||||
}
|
||||
return projectNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_URL_INDEXES_FOR_PROJECT_NAME;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.ProjectState;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class GetProjectState implements SQLQuery<ProjectState> {
|
||||
|
||||
private static final String GET_PROJECT_STATE =
|
||||
"SELECT `last_accessed`\n" +
|
||||
" FROM `projects`\n" +
|
||||
" WHERE `name` = ?";
|
||||
|
||||
private final String projectName;
|
||||
|
||||
public GetProjectState(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_PROJECT_STATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectState processResultSet(
|
||||
ResultSet resultSet
|
||||
) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
if (resultSet.getTimestamp("last_accessed") == null) {
|
||||
return ProjectState.SWAPPED;
|
||||
}
|
||||
return ProjectState.PRESENT;
|
||||
}
|
||||
return ProjectState.NOT_PRESENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class GetSwapCompression implements SQLQuery<String> {
|
||||
private static final String GET_SWAP_COMPRESSION =
|
||||
"SELECT `swap_compression` FROM `projects` WHERE `name` = ?";
|
||||
|
||||
private final String projectName;
|
||||
|
||||
public GetSwapCompression(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processResultSet(ResultSet resultSet) throws SQLException {
|
||||
String compression = null;
|
||||
while (resultSet.next()) {
|
||||
compression = resultSet.getString("swap_compression");
|
||||
}
|
||||
return compression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return GET_SWAP_COMPRESSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by winston on 04/09/2016.
|
||||
*/
|
||||
public class LastAccessedColumnExists implements SQLQuery<Boolean> {
|
||||
|
||||
private static final String LAST_ACCESSED_COLUMN_EXISTS =
|
||||
"PRAGMA table_info(`projects`)";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return LAST_ACCESSED_COLUMN_EXISTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
if (resultSet.getString(2).equals("last_accessed")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class RestoreTimeColumnExists implements SQLQuery<Boolean> {
|
||||
private static final String RESTORE_TIME_COLUMN_EXISTS =
|
||||
"PRAGMA table_info(`projects`)";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return RESTORE_TIME_COLUMN_EXISTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
if (resultSet.getString(2).equals("restore_time")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class SwapCompressionColumnExists implements SQLQuery<Boolean> {
|
||||
private static final String SWAP_COMPRESSION_COLUMN_EXISTS =
|
||||
"PRAGMA table_info(`projects`)";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return SWAP_COMPRESSION_COLUMN_EXISTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
if (resultSet.getString(2).equals("swap_compression")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.query;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLQuery;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class SwapTimeColumnExists implements SQLQuery<Boolean> {
|
||||
private static final String SWAP_TIME_COLUMN_EXISTS =
|
||||
"PRAGMA table_info(`projects`)";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return SWAP_TIME_COLUMN_EXISTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean processResultSet(ResultSet resultSet) throws SQLException {
|
||||
while (resultSet.next()) {
|
||||
if (resultSet.getString(2).equals("swap_time")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
/**
|
||||
* Created by winston on 03/09/2016.
|
||||
*/
|
||||
public class ProjectsAddLastAccessed implements SQLUpdate {
|
||||
|
||||
private static final String PROJECTS_ADD_LAST_ACCESSED =
|
||||
"ALTER TABLE `projects`\n" +
|
||||
"ADD COLUMN `last_accessed` DATETIME NULL DEFAULT 0";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return PROJECTS_ADD_LAST_ACCESSED;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
public class ProjectsAddRestoreTime implements SQLUpdate {
|
||||
private static final String PROJECTS_ADD_RESTORE_TIME =
|
||||
"ALTER TABLE `projects`\n" +
|
||||
"ADD COLUMN `restore_time` DATETIME NULL;\n";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return PROJECTS_ADD_RESTORE_TIME;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
public class ProjectsAddSwapCompression implements SQLUpdate {
|
||||
private static final String PROJECTS_ADD_SWAP_COMPRESSION =
|
||||
"ALTER TABLE `projects`\n" +
|
||||
"ADD COLUMN `swap_compression` VARCHAR NULL;\n";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return PROJECTS_ADD_SWAP_COMPRESSION;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
public class ProjectsAddSwapTime implements SQLUpdate {
|
||||
private static final String PROJECTS_ADD_SWAP_TIME =
|
||||
"ALTER TABLE `projects`\n" +
|
||||
"ADD COLUMN `swap_time` DATETIME NULL;\n";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return PROJECTS_ADD_SWAP_TIME;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.alter;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
public class SetSoftHeapLimitPragma implements SQLUpdate {
|
||||
private int heapLimitBytes = 0;
|
||||
|
||||
public SetSoftHeapLimitPragma(int heapLimitBytes) {
|
||||
this.heapLimitBytes = heapLimitBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return "PRAGMA soft_heap_limit="+this.heapLimitBytes+";";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.create;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
/**
|
||||
* Created by Winston on 21/02/15.
|
||||
*/
|
||||
public class CreateIndexURLIndexStore implements SQLUpdate {
|
||||
|
||||
public static final String CREATE_INDEX_URL_INDEX_STORE =
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `project_path_index` " +
|
||||
"ON `url_index_store`(`project_name`, `path`);\n";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return CREATE_INDEX_URL_INDEX_STORE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.create;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class CreateProjectsIndexLastAccessed implements SQLUpdate {
|
||||
|
||||
private static final String CREATE_PROJECTS_INDEX_LAST_ACCESSED =
|
||||
"CREATE INDEX IF NOT EXISTS `projects_index_last_accessed`\n" +
|
||||
" ON `projects`(`last_accessed`)";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return CREATE_PROJECTS_INDEX_LAST_ACCESSED;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.create;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class CreateProjectsTableSQLUpdate implements SQLUpdate {
|
||||
|
||||
private static final String CREATE_PROJECTS_TABLE =
|
||||
"CREATE TABLE IF NOT EXISTS `projects` (\n" +
|
||||
" `name` VARCHAR NOT NULL DEFAULT '',\n" +
|
||||
" `version_id` INT NOT NULL DEFAULT 0,\n" +
|
||||
" `last_accessed` DATETIME NULL DEFAULT 0,\n" +
|
||||
" `swap_time` DATETIME NULL,\n" +
|
||||
" `restore_time` DATETIME NULL,\n" +
|
||||
" `swap_compression` VARCHAR NULL,\n" +
|
||||
" PRIMARY KEY (`name`)\n" +
|
||||
")";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return CREATE_PROJECTS_TABLE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.create;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class CreateURLIndexStoreSQLUpdate implements SQLUpdate {
|
||||
|
||||
private static final String CREATE_URL_INDEX_STORE =
|
||||
"CREATE TABLE IF NOT EXISTS `url_index_store` (\n"+
|
||||
" `project_name` varchar(10) NOT NULL DEFAULT '',\n"+
|
||||
" `url` text NOT NULL,\n"+
|
||||
" `path` text NOT NULL,\n"+
|
||||
" PRIMARY KEY (`project_name`,`url`),\n"+
|
||||
" CONSTRAINT `url_index_store_ibfk_1` " +
|
||||
"FOREIGN KEY (`project_name`) " +
|
||||
"REFERENCES `projects` (`name`) " +
|
||||
"ON DELETE CASCADE " +
|
||||
"ON UPDATE CASCADE\n"+
|
||||
");\n";
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return CREATE_URL_INDEX_STORE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.delete;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class DeleteFilesForProjectSQLUpdate implements SQLUpdate {
|
||||
|
||||
private static final String DELETE_URL_INDEXES_FOR_PROJECT_NAME =
|
||||
"DELETE FROM `url_index_store` " +
|
||||
"WHERE `project_name` = ? AND path IN (";
|
||||
|
||||
private final String projectName;
|
||||
private final String[] paths;
|
||||
|
||||
public DeleteFilesForProjectSQLUpdate(
|
||||
String projectName,
|
||||
String... paths
|
||||
) {
|
||||
this.projectName = projectName;
|
||||
this.paths = paths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
StringBuilder sb = new StringBuilder(
|
||||
DELETE_URL_INDEXES_FOR_PROJECT_NAME
|
||||
);
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
sb.append("?");
|
||||
if (i < paths.length - 1) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
sb.append(");\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
statement.setString(i + 2, paths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class AddURLIndexSQLUpdate implements SQLUpdate {
|
||||
|
||||
private static final String ADD_URL_INDEX =
|
||||
"INSERT OR REPLACE INTO `url_index_store`(" +
|
||||
"`project_name`, " +
|
||||
"`url`, " +
|
||||
"`path`" +
|
||||
") VALUES " +
|
||||
"(?, ?, ?)\n";
|
||||
|
||||
private final String projectName;
|
||||
private final String url;
|
||||
private final String path;
|
||||
|
||||
public AddURLIndexSQLUpdate(String projectName, String url, String path) {
|
||||
this.projectName = projectName;
|
||||
this.url = url;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return ADD_URL_INDEX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
statement.setString(2, url);
|
||||
statement.setString(3, path);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class SetProjectLastAccessedTime implements SQLUpdate {
|
||||
|
||||
private static final String SET_PROJECT_LAST_ACCESSED_TIME =
|
||||
"UPDATE `projects`\n" +
|
||||
"SET `last_accessed` = ?\n" +
|
||||
"WHERE `name` = ?";
|
||||
|
||||
private final String projectName;
|
||||
private final Timestamp lastAccessed;
|
||||
|
||||
public SetProjectLastAccessedTime(
|
||||
String projectName,
|
||||
Timestamp lastAccessed
|
||||
) {
|
||||
this.projectName = projectName;
|
||||
this.lastAccessed = lastAccessed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return SET_PROJECT_LAST_ACCESSED_TIME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setTimestamp(1, lastAccessed);
|
||||
statement.setString(2, projectName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class SetProjectSQLUpdate implements SQLUpdate {
|
||||
|
||||
private static final String SET_PROJECT =
|
||||
"INSERT OR REPLACE "
|
||||
+ "INTO `projects`(`name`, `version_id`, `last_accessed`) "
|
||||
+ "VALUES (?, ?, DATETIME('now'));\n";
|
||||
|
||||
private final String projectName;
|
||||
private final int versionID;
|
||||
|
||||
public SetProjectSQLUpdate(String projectName, int versionID) {
|
||||
this.projectName = projectName;
|
||||
this.versionID = versionID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return SET_PROJECT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setString(1, projectName);
|
||||
statement.setInt(2, versionID);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class UpdateRestore implements SQLUpdate {
|
||||
private static final String UPDATE_RESTORE =
|
||||
"UPDATE `projects`\n" +
|
||||
"SET `last_accessed` = ?,\n" +
|
||||
" `swap_time` = NULL,\n" +
|
||||
" `restore_time` = ?,\n" +
|
||||
" `swap_compression` = NULL\n" +
|
||||
"WHERE `name` = ?;\n";
|
||||
|
||||
private final String projectName;
|
||||
private final Timestamp now;
|
||||
|
||||
public UpdateRestore(String projectName) {
|
||||
this.projectName = projectName;
|
||||
this.now = Timestamp.valueOf(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return UPDATE_RESTORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setTimestamp(1, now);
|
||||
statement.setTimestamp(2, now);
|
||||
statement.setString(3, projectName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.db.sqlite.update.insert;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.sqlite.SQLUpdate;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class UpdateSwap implements SQLUpdate {
|
||||
private static final String UPDATE_SWAP =
|
||||
"UPDATE `projects`\n" +
|
||||
"SET `last_accessed` = NULL,\n" +
|
||||
" `swap_time` = ?,\n" +
|
||||
" `restore_time` = NULL,\n" +
|
||||
" `swap_compression` = ?\n" +
|
||||
"WHERE `name` = ?;\n";
|
||||
|
||||
private final String projectName;
|
||||
private final String compression;
|
||||
private final Timestamp now;
|
||||
|
||||
public UpdateSwap(String projectName, String compression) {
|
||||
this.projectName = projectName;
|
||||
this.compression = compression;
|
||||
this.now = Timestamp.valueOf(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSQL() {
|
||||
return UPDATE_SWAP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParametersToStatement(
|
||||
PreparedStatement statement
|
||||
) throws SQLException {
|
||||
statement.setTimestamp(1, now);
|
||||
statement.setString(2, compression);
|
||||
statement.setString(3, projectName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.gc;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.Bridge;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.ProjectRepo;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Is started by the bridge. Every time a project is updated, we queue it for
|
||||
* GC which executes every hour or so.
|
||||
*
|
||||
* We don't queue it into a more immediate Executor because there is no way to
|
||||
* know if a call to {@link Bridge#updateProject(Optional, ProjectRepo)},
|
||||
* which releases the lock, is going to call
|
||||
* {@link Bridge#push(Optional, String, RawDirectory, RawDirectory, String)}.
|
||||
*
|
||||
* We don't want the GC to run in between an update and a push.
|
||||
*/
|
||||
public interface GcJob {
|
||||
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
void onPreGc(Runnable preGc);
|
||||
|
||||
void onPostGc(Runnable postGc);
|
||||
|
||||
void queueForGc(String projectName);
|
||||
|
||||
CompletableFuture<Void> waitForRun();
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.gc;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.LockGuard;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.ProjectRepo;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.TimerUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Implementation of {@link GcJob} using its own Timer and a synchronized
|
||||
* queue.
|
||||
*/
|
||||
public class GcJobImpl implements GcJob {
|
||||
|
||||
private final RepoStore repoStore;
|
||||
private final ProjectLock locks;
|
||||
|
||||
private final long intervalMs;
|
||||
private final Timer timer;
|
||||
|
||||
private final Set<String> gcQueue;
|
||||
|
||||
/**
|
||||
* Hooks in case they are needed, e.g. for testing.
|
||||
*/
|
||||
private AtomicReference<Runnable> preGc;
|
||||
private AtomicReference<Runnable> postGc;
|
||||
|
||||
/* We need to iterate over and empty it after every run */
|
||||
private final Lock jobWaitersLock;
|
||||
private final List<CompletableFuture<Void>> jobWaiters;
|
||||
|
||||
public GcJobImpl(RepoStore repoStore, ProjectLock locks, long intervalMs) {
|
||||
this.repoStore = repoStore;
|
||||
this.locks = locks;
|
||||
this.intervalMs = intervalMs;
|
||||
timer = new Timer();
|
||||
gcQueue = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
preGc = new AtomicReference<>(() -> {});
|
||||
postGc = new AtomicReference<>(() -> {});
|
||||
jobWaitersLock = new ReentrantLock();
|
||||
jobWaiters = new ArrayList<>();
|
||||
}
|
||||
|
||||
public GcJobImpl(RepoStore repoStore, ProjectLock locks) {
|
||||
this(
|
||||
repoStore,
|
||||
locks,
|
||||
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
Log.info("Starting GC job to run every [{}] ms", intervalMs);
|
||||
timer.scheduleAtFixedRate(
|
||||
TimerUtils.makeTimerTask(this::doGC),
|
||||
intervalMs,
|
||||
intervalMs
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
Log.info("Stopping GC job");
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreGc(Runnable preGc) {
|
||||
this.preGc.set(preGc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostGc(Runnable postGc) {
|
||||
this.postGc.set(postGc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be callable from any thread.
|
||||
* @param projectName
|
||||
*/
|
||||
@Override
|
||||
public void queueForGc(String projectName) {
|
||||
gcQueue.add(projectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> waitForRun() {
|
||||
CompletableFuture<Void> ret = new CompletableFuture<>();
|
||||
jobWaitersLock.lock();
|
||||
try {
|
||||
jobWaiters.add(ret);
|
||||
} finally {
|
||||
jobWaitersLock.unlock();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void doGC() {
|
||||
Log.info("GC job running");
|
||||
int numGcs = 0;
|
||||
preGc.get().run();
|
||||
for (
|
||||
Iterator<String> it = gcQueue.iterator();
|
||||
it.hasNext();
|
||||
it.remove(), ++numGcs
|
||||
) {
|
||||
String proj = it.next();
|
||||
Log.info("[{}] Running GC job on project", proj);
|
||||
try (LockGuard __ = locks.lockGuard(proj)) {
|
||||
try {
|
||||
ProjectRepo repo = repoStore.getExistingRepo(proj);
|
||||
repo.runGC();
|
||||
repo.deleteIncomingPacks();
|
||||
} catch (IOException e) {
|
||||
Log.info("[{}] Failed to GC project", proj);
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.info("GC job finished, num gcs: {}", numGcs);
|
||||
jobWaitersLock.lock();
|
||||
try {
|
||||
jobWaiters.forEach(w -> w.complete(null));
|
||||
} finally {
|
||||
jobWaitersLock.unlock();
|
||||
}
|
||||
postGc.get().run();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.lock;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public interface LockGuard extends AutoCloseable {
|
||||
|
||||
void close();
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.lock;
|
||||
|
||||
/**
|
||||
* Project Lock class.
|
||||
*
|
||||
* The locks should be re-entrant. For example, we are usually holding the lock
|
||||
* when a project must be restored, which tries to acquire the lock again.
|
||||
*/
|
||||
public interface ProjectLock {
|
||||
|
||||
void lockAll();
|
||||
|
||||
void lockForProject(String projectName);
|
||||
|
||||
void unlockForProject(String projectName);
|
||||
|
||||
/* RAII hahaha */
|
||||
default LockGuard lockGuard(String projectName) {
|
||||
lockForProject(projectName);
|
||||
return () -> unlockForProject(projectName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import com.google.api.client.repackaged.com.google.common.base.Preconditions;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.Project;
|
||||
import uk.ac.ic.wlgitbridge.util.Tar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static uk.ac.ic.wlgitbridge.util.Util.deleteInDirectoryApartFrom;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public class FSGitRepoStore implements RepoStore {
|
||||
|
||||
private static final long DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
private final String repoStorePath;
|
||||
|
||||
private final File rootDirectory;
|
||||
|
||||
private final long maxFileSize;
|
||||
|
||||
private final Function<File, Long> fsSizer;
|
||||
|
||||
public FSGitRepoStore(
|
||||
String repoStorePath,
|
||||
Optional<Long> maxFileSize
|
||||
) {
|
||||
this(
|
||||
repoStorePath,
|
||||
maxFileSize.orElse(DEFAULT_MAX_FILE_SIZE),
|
||||
d -> d.getTotalSpace() - d.getFreeSpace()
|
||||
);
|
||||
}
|
||||
|
||||
public FSGitRepoStore(
|
||||
String repoStorePath,
|
||||
long maxFileSize,
|
||||
Function<File, Long> fsSizer
|
||||
) {
|
||||
this.repoStorePath = repoStorePath;
|
||||
rootDirectory = initRootGitDirectory(repoStorePath);
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.fsSizer = fsSizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepoStorePath() {
|
||||
return repoStorePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRootDirectory() {
|
||||
return rootDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectRepo initRepo(String project) throws IOException {
|
||||
GitProjectRepo ret = GitProjectRepo.fromName(project);
|
||||
ret.initRepo(this);
|
||||
return new WalkOverrideGitRepo(
|
||||
ret, Optional.of(maxFileSize), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectRepo initRepoFromExisting(
|
||||
String project, String fromProject
|
||||
) throws IOException {
|
||||
String repoRoot = getRepoStorePath();
|
||||
String sourcePath = repoRoot + "/" + fromProject;
|
||||
String destinationPath = repoRoot + "/" + project;
|
||||
Log.info("[{}] Init repo by copying data from: {}, to: {}",
|
||||
project,
|
||||
sourcePath,
|
||||
destinationPath
|
||||
);
|
||||
File source = new File(sourcePath);
|
||||
File destination = new File(destinationPath);
|
||||
FileUtils.copyDirectory(source, destination);
|
||||
GitProjectRepo ret = GitProjectRepo.fromName(project);
|
||||
ret.useExistingRepository(this);
|
||||
return new WalkOverrideGitRepo(
|
||||
ret, Optional.of(maxFileSize), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectRepo getExistingRepo(String project) throws IOException {
|
||||
GitProjectRepo ret = GitProjectRepo.fromName(project);
|
||||
ret.useExistingRepository(this);
|
||||
return new WalkOverrideGitRepo(
|
||||
ret, Optional.of(maxFileSize), Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectRepo useJGitRepo(Repository repo, ObjectId commitId) {
|
||||
GitProjectRepo ret = GitProjectRepo.fromJGitRepo(repo);
|
||||
return new WalkOverrideGitRepo(
|
||||
ret, Optional.of(maxFileSize), Optional.of(commitId));
|
||||
}
|
||||
|
||||
/* TODO: Perhaps we should just delete bad directories on the fly. */
|
||||
@Override
|
||||
public void purgeNonexistentProjects(
|
||||
Collection<String> existingProjectNames
|
||||
) {
|
||||
List<String> excludedFromDeletion =
|
||||
new ArrayList<>(existingProjectNames);
|
||||
excludedFromDeletion.add(".wlgb");
|
||||
deleteInDirectoryApartFrom(
|
||||
rootDirectory,
|
||||
excludedFromDeletion.toArray(new String[] {})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long totalSize() {
|
||||
return fsSizer.apply(rootDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream bzip2Project(
|
||||
String projectName,
|
||||
long[] sizePtr
|
||||
) throws IOException {
|
||||
Project.checkValidProjectName(projectName);
|
||||
Log.info("[{}] bzip2 project", projectName);
|
||||
return Tar.bz2.zip(getDotGitForProject(projectName), sizePtr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream gzipProject(
|
||||
String projectName,
|
||||
long[] sizePtr
|
||||
) throws IOException {
|
||||
Project.checkValidProjectName(projectName);
|
||||
Log.info("[{}] gzip project", projectName);
|
||||
return Tar.gzip.zip(getDotGitForProject(projectName), sizePtr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gcProject(String projectName) throws IOException {
|
||||
Project.checkValidProjectName(projectName);
|
||||
ProjectRepo repo = getExistingRepo(projectName);
|
||||
repo.runGC();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String projectName) throws IOException {
|
||||
Project.checkValidProjectName(projectName);
|
||||
FileUtils.deleteDirectory(new File(rootDirectory, projectName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbzip2Project(
|
||||
String projectName,
|
||||
InputStream dataStream
|
||||
) throws IOException {
|
||||
Preconditions.checkArgument(
|
||||
Project.isValidProjectName(projectName),
|
||||
"[%s] invalid project name: ",
|
||||
projectName
|
||||
);
|
||||
Preconditions.checkState(
|
||||
getDirForProject(projectName).mkdirs(),
|
||||
"[%s] directories for " +
|
||||
"evicted project already exist",
|
||||
projectName
|
||||
);
|
||||
Log.info("[{}] un-bzip2 project", projectName);
|
||||
Tar.bz2.unzip(dataStream, getDirForProject(projectName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ungzipProject(
|
||||
String projectName,
|
||||
InputStream dataStream
|
||||
) throws IOException {
|
||||
Preconditions.checkArgument(
|
||||
Project.isValidProjectName(projectName),
|
||||
"[%s] invalid project name: ",
|
||||
projectName
|
||||
);
|
||||
Preconditions.checkState(
|
||||
getDirForProject(projectName).mkdirs(),
|
||||
"[%s] directories for " +
|
||||
"evicted project already exist",
|
||||
projectName
|
||||
);
|
||||
Log.info("[{}] un-gzip project", projectName);
|
||||
Tar.gzip.unzip(dataStream, getDirForProject(projectName));
|
||||
}
|
||||
|
||||
private File getDirForProject(String projectName) {
|
||||
Project.checkValidProjectName(projectName);
|
||||
return Paths.get(
|
||||
rootDirectory.getAbsolutePath()
|
||||
).resolve(
|
||||
projectName
|
||||
).toFile();
|
||||
}
|
||||
|
||||
private File getDotGitForProject(String projectName) {
|
||||
Project.checkValidProjectName(projectName);
|
||||
return Paths.get(
|
||||
rootDirectory.getAbsolutePath()
|
||||
).resolve(
|
||||
projectName
|
||||
).resolve(
|
||||
".git"
|
||||
).toFile();
|
||||
}
|
||||
|
||||
private File initRootGitDirectory(String rootGitDirectoryPath) {
|
||||
File rootGitDirectory = new File(rootGitDirectoryPath);
|
||||
rootGitDirectory.mkdirs();
|
||||
Preconditions.checkArgument(
|
||||
rootGitDirectory.isDirectory(),
|
||||
"given root git directory " +
|
||||
"is not a directory: %s",
|
||||
rootGitDirectory.getAbsolutePath()
|
||||
);
|
||||
return rootGitDirectory;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.ResetCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
import uk.ac.ic.wlgitbridge.git.util.RepositoryObjectTreeWalker;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.Project;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class representing a Git repository.
|
||||
*
|
||||
* It stores the projectName and repo separately because the hooks need to be
|
||||
* able to construct one of these without knowing whether the repo exists yet.
|
||||
*
|
||||
* It can then be passed to the Bridge, which will either
|
||||
* {@link #initRepo(RepoStore)} for a never-seen-before repo, or
|
||||
* {@link #useExistingRepository(RepoStore)} for an existing repo.
|
||||
*
|
||||
* Make sure to acquire the project lock before calling methods here.
|
||||
*/
|
||||
public class GitProjectRepo implements ProjectRepo {
|
||||
|
||||
private final String projectName;
|
||||
private Optional<Repository> repository;
|
||||
|
||||
public static GitProjectRepo fromJGitRepo(Repository repo) {
|
||||
return new GitProjectRepo(
|
||||
repo.getWorkTree().getName(), Optional.of(repo));
|
||||
}
|
||||
|
||||
public static GitProjectRepo fromName(String projectName) {
|
||||
return new GitProjectRepo(projectName, Optional.empty());
|
||||
}
|
||||
|
||||
GitProjectRepo(String projectName, Optional<Repository> repository) {
|
||||
Preconditions.checkArgument(Project.isValidProjectName(projectName));
|
||||
this.projectName = projectName;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initRepo(
|
||||
RepoStore repoStore
|
||||
) throws IOException {
|
||||
initRepositoryField(repoStore);
|
||||
Preconditions.checkState(repository.isPresent());
|
||||
Repository repo = this.repository.get();
|
||||
// TODO: assert that this is a fresh repo. At the moment, we can't be
|
||||
// sure whether the repo to be init'd doesn't exist or is just fresh
|
||||
// and we crashed / aborted while committing
|
||||
if (repo.getObjectDatabase().exists()) return;
|
||||
repo.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void useExistingRepository(
|
||||
RepoStore repoStore
|
||||
) throws IOException {
|
||||
initRepositoryField(repoStore);
|
||||
Preconditions.checkState(repository.isPresent());
|
||||
Preconditions.checkState(
|
||||
repository.get().getObjectDatabase().exists()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RawDirectory getDirectory()
|
||||
throws IOException, GitUserException {
|
||||
Preconditions.checkState(repository.isPresent());
|
||||
return new RepositoryObjectTreeWalker(
|
||||
repository.get()
|
||||
).getDirectoryContents(Optional.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> commitAndGetMissing(
|
||||
GitDirectoryContents contents
|
||||
) throws IOException {
|
||||
try {
|
||||
return doCommitAndGetMissing(contents);
|
||||
} catch (GitAPIException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runGC() throws IOException {
|
||||
Preconditions.checkState(
|
||||
repository.isPresent(),
|
||||
"Repo is not present"
|
||||
);
|
||||
File dir = getProjectDir();
|
||||
Preconditions.checkState(dir.isDirectory());
|
||||
Log.info("[{}] Running git gc", projectName);
|
||||
Process proc = new ProcessBuilder(
|
||||
"git", "gc"
|
||||
).directory(dir).start();
|
||||
int exitCode;
|
||||
try {
|
||||
exitCode = proc.waitFor();
|
||||
Log.info("Exit: {}", exitCode);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (exitCode != 0) {
|
||||
Log.warn("[{}] Git gc failed", dir.getAbsolutePath());
|
||||
Log.warn(IOUtils.toString(
|
||||
proc.getInputStream(),
|
||||
StandardCharsets.UTF_8
|
||||
));
|
||||
Log.warn(IOUtils.toString(
|
||||
proc.getErrorStream(),
|
||||
StandardCharsets.UTF_8
|
||||
));
|
||||
throw new IOException("git gc error");
|
||||
}
|
||||
Log.info("[{}] git gc successful", projectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteIncomingPacks() throws IOException {
|
||||
Log.info(
|
||||
"[{}] Checking for garbage `incoming` files",
|
||||
projectName
|
||||
);
|
||||
Files.walkFileTree(getDotGitDir().toPath(), new FileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(
|
||||
Path dir,
|
||||
BasicFileAttributes attrs
|
||||
) throws IOException {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(
|
||||
Path file,
|
||||
BasicFileAttributes attrs
|
||||
) throws IOException {
|
||||
File file_ = file.toFile();
|
||||
String name = file_.getName();
|
||||
if (name.startsWith("incoming_") && name.endsWith(".pack")) {
|
||||
Log.info("Deleting garbage `incoming` file: {}", file_);
|
||||
Preconditions.checkState(file_.delete());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(
|
||||
Path file,
|
||||
IOException exc
|
||||
) throws IOException {
|
||||
Preconditions.checkNotNull(file);
|
||||
Preconditions.checkNotNull(exc);
|
||||
Log.warn("Failed to visit file: " + file, exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(
|
||||
Path dir,
|
||||
IOException exc
|
||||
) throws IOException {
|
||||
Preconditions.checkNotNull(dir);
|
||||
if (exc != null) {
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getProjectDir() {
|
||||
return getJGitRepository().getDirectory().getParentFile();
|
||||
}
|
||||
|
||||
public void resetHard() throws IOException {
|
||||
Git git = new Git(getJGitRepository());
|
||||
try {
|
||||
git.reset().setMode(ResetCommand.ResetType.HARD).call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getJGitRepository() {
|
||||
return repository.get();
|
||||
}
|
||||
|
||||
public File getDotGitDir() {
|
||||
return getJGitRepository().getWorkTree();
|
||||
}
|
||||
|
||||
private void initRepositoryField(RepoStore repoStore) throws IOException {
|
||||
Preconditions.checkNotNull(repoStore);
|
||||
Preconditions.checkArgument(Project.isValidProjectName(projectName));
|
||||
Preconditions.checkState(!repository.isPresent());
|
||||
repository = Optional.of(createJGitRepository(repoStore, projectName));
|
||||
}
|
||||
|
||||
private Repository createJGitRepository(
|
||||
RepoStore repoStore,
|
||||
String projName
|
||||
) throws IOException {
|
||||
File repoDir = new File(repoStore.getRootDirectory(), projName);
|
||||
return new FileRepositoryBuilder().setWorkTree(repoDir).build();
|
||||
}
|
||||
|
||||
private Collection<String> doCommitAndGetMissing(
|
||||
GitDirectoryContents contents
|
||||
) throws IOException, GitAPIException {
|
||||
Preconditions.checkState(repository.isPresent());
|
||||
Repository repo = getJGitRepository();
|
||||
resetHard();
|
||||
String name = getProjectName();
|
||||
Log.info("[{}] Writing commit", name);
|
||||
contents.write();
|
||||
Git git = new Git(getJGitRepository());
|
||||
Log.info("[{}] Getting missing files", name);
|
||||
Set<String> missingFiles = git.status().call().getMissing();
|
||||
for (String missing : missingFiles) {
|
||||
Log.info("[{}] Git rm {}", name, missing);
|
||||
git.rm().setCached(true).addFilepattern(missing).call();
|
||||
}
|
||||
Log.info("[{}] Calling Git add", name);
|
||||
git.add(
|
||||
).setWorkingTreeIterator(
|
||||
new NoGitignoreIterator(repo)
|
||||
).addFilepattern(".").call();
|
||||
Log.info("[{}] Calling Git commit", name);
|
||||
git.commit(
|
||||
).setAuthor(
|
||||
new PersonIdent(
|
||||
contents.getUserName(),
|
||||
contents.getUserEmail(),
|
||||
contents.getWhen(),
|
||||
TimeZone.getDefault()
|
||||
)
|
||||
).setMessage(
|
||||
contents.getCommitMessage()
|
||||
).call();
|
||||
Log.info(
|
||||
"[{}] Deleting files in directory: {}",
|
||||
name,
|
||||
contents.getDirectory().getAbsolutePath()
|
||||
);
|
||||
Util.deleteInDirectoryApartFrom(contents.getDirectory(), ".git");
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
|
||||
import org.eclipse.jgit.treewalk.FileTreeIterator;
|
||||
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
|
||||
import org.eclipse.jgit.treewalk.WorkingTreeOptions;
|
||||
import org.eclipse.jgit.util.FS;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Created by winston on 08/10/2016.
|
||||
*/
|
||||
public class NoGitignoreIterator extends FileTreeIterator {
|
||||
|
||||
private static final Field ignoreNodeField;
|
||||
|
||||
static {
|
||||
try {
|
||||
ignoreNodeField = WorkingTreeIterator.class.getDeclaredField(
|
||||
"ignoreNode"
|
||||
);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ignoreNodeField.setAccessible(true);
|
||||
}
|
||||
|
||||
public NoGitignoreIterator(Repository repo) {
|
||||
super(repo);
|
||||
}
|
||||
|
||||
public NoGitignoreIterator(
|
||||
Repository repo,
|
||||
FileModeStrategy fileModeStrategy
|
||||
) {
|
||||
super(repo, fileModeStrategy);
|
||||
}
|
||||
|
||||
public NoGitignoreIterator(File root, FS fs, WorkingTreeOptions options) {
|
||||
super(root, fs, options);
|
||||
}
|
||||
|
||||
public NoGitignoreIterator(
|
||||
File root,
|
||||
FS fs,
|
||||
WorkingTreeOptions options,
|
||||
FileModeStrategy fileModeStrategy
|
||||
) {
|
||||
super(root, fs, options, fileModeStrategy);
|
||||
}
|
||||
|
||||
protected NoGitignoreIterator(FileTreeIterator p, File root, FS fs) {
|
||||
super(p, root, fs);
|
||||
}
|
||||
|
||||
protected NoGitignoreIterator(
|
||||
WorkingTreeIterator p,
|
||||
File root,
|
||||
FS fs,
|
||||
FileModeStrategy fileModeStrategy
|
||||
) {
|
||||
super(p, root, fs, fileModeStrategy);
|
||||
}
|
||||
|
||||
// Note: the `list` is a list of top-level entities in this directory,
|
||||
// not a full list of files in the tree.
|
||||
@Override
|
||||
protected void init(Entry[] list) {
|
||||
super.init(list);
|
||||
try {
|
||||
ignoreNodeField.set(this, null);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// When entering a sub-directory, create a new instance of this class,
|
||||
// so we can also ignore gitignore specifications in sub-directories
|
||||
@Override
|
||||
protected AbstractTreeIterator enterSubtree() {
|
||||
String fullPath = getDirectory().getAbsolutePath() + "/" + current().getName();
|
||||
return new NoGitignoreIterator(this, new File(fullPath), fs, fileModeStrategy);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface ProjectRepo {
|
||||
|
||||
String getProjectName();
|
||||
|
||||
void initRepo(
|
||||
RepoStore repoStore
|
||||
) throws IOException;
|
||||
|
||||
void useExistingRepository(
|
||||
RepoStore repoStore
|
||||
) throws IOException;
|
||||
|
||||
RawDirectory getDirectory(
|
||||
) throws IOException, GitUserException;
|
||||
|
||||
Collection<String> commitAndGetMissing(
|
||||
GitDirectoryContents gitDirectoryContents
|
||||
) throws IOException, GitUserException;
|
||||
|
||||
void runGC() throws IOException;
|
||||
|
||||
void deleteIncomingPacks() throws IOException;
|
||||
|
||||
File getProjectDir();
|
||||
|
||||
Repository getJGitRepository();
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface RepoStore {
|
||||
|
||||
/* Still need to get rid of these two methods.
|
||||
Main dependency: GitRepoStore needs a Repository which needs a directory.
|
||||
Instead, use a visitor or something. */
|
||||
String getRepoStorePath();
|
||||
|
||||
File getRootDirectory();
|
||||
|
||||
ProjectRepo initRepo(String project) throws IOException;
|
||||
|
||||
ProjectRepo initRepoFromExisting(String project, String fromProject) throws IOException;
|
||||
|
||||
ProjectRepo getExistingRepo(String project) throws IOException;
|
||||
|
||||
ProjectRepo useJGitRepo(Repository repo, ObjectId commitId);
|
||||
|
||||
void purgeNonexistentProjects(
|
||||
Collection<String> existingProjectNames
|
||||
);
|
||||
|
||||
long totalSize();
|
||||
|
||||
/**
|
||||
* Tars and bzip2s the .git directory of the given project. Throws an
|
||||
* IOException if the project doesn't exist. The returned stream is a copy
|
||||
* of the original .git directory, which must be deleted using remove().
|
||||
*/
|
||||
InputStream bzip2Project(
|
||||
String projectName,
|
||||
long[] sizePtr
|
||||
) throws IOException;
|
||||
|
||||
default InputStream bzip2Project(
|
||||
String projectName
|
||||
) throws IOException {
|
||||
return bzip2Project(projectName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tars and gzips the .git directory of the given project. Throws an
|
||||
* IOException if the project doesn't exist. The returned stream is a copy
|
||||
* of the original .git directory, which must be deleted using remove().
|
||||
*/
|
||||
InputStream gzipProject(
|
||||
String projectName,
|
||||
long[] sizePtr
|
||||
) throws IOException;
|
||||
|
||||
default InputStream gzipProject(
|
||||
String projectName
|
||||
) throws IOException {
|
||||
return gzipProject(projectName, null);
|
||||
}
|
||||
|
||||
void gcProject(String projectName) throws IOException;
|
||||
|
||||
/**
|
||||
* Called after {@link #bzip2Project(String, long[])}'s has been safely
|
||||
* uploaded to the swap store. Removes all traces of the project from disk,
|
||||
* i.e. not just its .git, but the whole project's git directory.
|
||||
* @param projectName
|
||||
* @throws IOException
|
||||
*/
|
||||
void remove(String projectName) throws IOException;
|
||||
|
||||
/**
|
||||
* Unbzip2s the given data stream into a .git directory for projectName.
|
||||
* Creates the project's git directory.
|
||||
* If projectName already exists, throws an IOException.
|
||||
* @param projectName the name of the project, e.g. abc123
|
||||
* @param dataStream the data stream containing the bzipped contents.
|
||||
*/
|
||||
void unbzip2Project(
|
||||
String projectName,
|
||||
InputStream dataStream
|
||||
) throws IOException;
|
||||
|
||||
/**
|
||||
* Ungzips the given data stream into a .git directory for projectName.
|
||||
* Creates the project's git directory.
|
||||
* If projectName already exists, throws an IOException.
|
||||
* @param projectName the name of the project, e.g. abc123
|
||||
* @param dataStream the data stream containing the gzip contents.
|
||||
*/
|
||||
void ungzipProject(
|
||||
String projectName,
|
||||
InputStream dataStream
|
||||
) throws IOException;
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by winston on 02/07/2017.
|
||||
*/
|
||||
public class RepoStoreConfig {
|
||||
|
||||
@Nullable
|
||||
private final Long maxFileSize;
|
||||
|
||||
@Nullable
|
||||
private final Long maxFileNum;
|
||||
|
||||
public RepoStoreConfig(Long maxFileSize, Long maxFileNum) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.maxFileNum = maxFileNum;
|
||||
}
|
||||
|
||||
public Optional<Long> getMaxFileSize() {
|
||||
return Optional.ofNullable(maxFileSize);
|
||||
}
|
||||
|
||||
public Optional<Long> getMaxFileNum() {
|
||||
return Optional.ofNullable(maxFileNum);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.repo;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
import uk.ac.ic.wlgitbridge.git.util.RepositoryObjectTreeWalker;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class takes a GitProjectRepo and delegates all calls to it.
|
||||
*
|
||||
* The purpose is to insert a file size check in {@link #getDirectory()}.
|
||||
*
|
||||
* We delegate instead of subclass because we can't override the static
|
||||
* constructors in {@link GitProjectRepo}.
|
||||
*/
|
||||
public class WalkOverrideGitRepo implements ProjectRepo {
|
||||
|
||||
private final GitProjectRepo gitRepo;
|
||||
|
||||
private final Optional<Long> maxFileSize;
|
||||
|
||||
private final Optional<ObjectId> commitId;
|
||||
|
||||
public WalkOverrideGitRepo(
|
||||
GitProjectRepo gitRepo,
|
||||
Optional<Long> maxFileSize,
|
||||
Optional<ObjectId> commitId
|
||||
) {
|
||||
this.gitRepo = gitRepo;
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.commitId = commitId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProjectName() {
|
||||
return gitRepo.getProjectName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initRepo(RepoStore repoStore) throws IOException {
|
||||
gitRepo.initRepo(repoStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void useExistingRepository(RepoStore repoStore) throws IOException {
|
||||
gitRepo.useExistingRepository(repoStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RawDirectory getDirectory() throws IOException, GitUserException {
|
||||
Repository repo = gitRepo.getJGitRepository();
|
||||
RepositoryObjectTreeWalker walker;
|
||||
if (commitId.isPresent()) {
|
||||
walker = new RepositoryObjectTreeWalker(repo, commitId.get());
|
||||
} else {
|
||||
walker = new RepositoryObjectTreeWalker(repo);
|
||||
}
|
||||
return walker.getDirectoryContents(maxFileSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> commitAndGetMissing(
|
||||
GitDirectoryContents gitDirectoryContents
|
||||
) throws GitUserException, IOException {
|
||||
return gitRepo.commitAndGetMissing(gitDirectoryContents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runGC() throws IOException {
|
||||
gitRepo.runGC();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteIncomingPacks() throws IOException {
|
||||
gitRepo.deleteIncomingPacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getProjectDir() {
|
||||
return gitRepo.getProjectDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getJGitRepository() {
|
||||
return gitRepo.getJGitRepository();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.resource;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawFile;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface ResourceCache {
|
||||
|
||||
RawFile get(
|
||||
String projectName,
|
||||
String url,
|
||||
String newPath,
|
||||
Map<String, RawFile> fileTable,
|
||||
Map<String, byte[]> fetchedUrls,
|
||||
Optional<Long> maxFileSize
|
||||
) throws IOException, SizeLimitExceededException;
|
||||
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.resource;
|
||||
|
||||
import static org.asynchttpclient.Dsl.*;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawFile;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RepositoryFile;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException;
|
||||
import uk.ac.ic.wlgitbridge.io.http.ning.NingHttpClient;
|
||||
import uk.ac.ic.wlgitbridge.io.http.ning.NingHttpClientFacade;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public class UrlResourceCache implements ResourceCache {
|
||||
|
||||
private final DBStore dbStore;
|
||||
|
||||
private final NingHttpClientFacade http;
|
||||
|
||||
UrlResourceCache(DBStore dbStore, NingHttpClientFacade http) {
|
||||
this.dbStore = dbStore;
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
public UrlResourceCache(DBStore dbStore) {
|
||||
this(dbStore, new NingHttpClient(asyncHttpClient()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RawFile get(
|
||||
String projectName,
|
||||
String url,
|
||||
String newPath,
|
||||
Map<String, RawFile> fileTable,
|
||||
Map<String, byte[]> fetchedUrls,
|
||||
Optional<Long> maxFileSize
|
||||
) throws IOException, SizeLimitExceededException {
|
||||
String path = dbStore.getPathForURLInProject(projectName, getCacheKeyFromUrl(url));
|
||||
byte[] contents;
|
||||
if (path == null) {
|
||||
path = newPath;
|
||||
contents = fetch(projectName, url, path, maxFileSize);
|
||||
fetchedUrls.put(url, contents);
|
||||
} else {
|
||||
Log.info("Found (" + projectName + "): " + url);
|
||||
Log.info("At (" + projectName + "): " + path);
|
||||
contents = fetchedUrls.get(url);
|
||||
if (contents == null) {
|
||||
RawFile rawFile = fileTable.get(path);
|
||||
if (rawFile == null) {
|
||||
Log.warn(
|
||||
"File " + path
|
||||
+ " was not in the current commit, "
|
||||
+ "or the git tree, yet path was not null. "
|
||||
+ "File url is: "
|
||||
+ url
|
||||
);
|
||||
contents = fetch(projectName, url, path, maxFileSize);
|
||||
} else {
|
||||
contents = rawFile.getContents();
|
||||
}
|
||||
}
|
||||
}
|
||||
return new RepositoryFile(newPath, contents);
|
||||
}
|
||||
|
||||
private byte[] fetch(
|
||||
String projectName,
|
||||
final String url,
|
||||
String path,
|
||||
Optional<Long> maxFileSize
|
||||
) throws FailedConnectionException, SizeLimitExceededException {
|
||||
byte[] contents;
|
||||
Log.info("GET -> " + url);
|
||||
try {
|
||||
contents = http.get(url, hs -> {
|
||||
List<String> contentLengths = hs.getAll("Content-Length");
|
||||
if (!maxFileSize.isPresent()) {
|
||||
return true;
|
||||
}
|
||||
if (contentLengths.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
long contentLength = Long.parseLong(contentLengths.get(0));
|
||||
long maxFileSize_ = maxFileSize.get();
|
||||
if (contentLength <= maxFileSize_) {
|
||||
return true;
|
||||
}
|
||||
throw new SizeLimitExceededException(
|
||||
Optional.of(path), contentLength, maxFileSize_
|
||||
);
|
||||
});
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof SizeLimitExceededException) {
|
||||
throw (SizeLimitExceededException) cause;
|
||||
}
|
||||
Log.warn(
|
||||
"ExecutionException when fetching project: " +
|
||||
projectName +
|
||||
", url: " +
|
||||
url +
|
||||
", path: " +
|
||||
path,
|
||||
e
|
||||
);
|
||||
throw new FailedConnectionException();
|
||||
}
|
||||
if (maxFileSize.isPresent() && contents.length > maxFileSize.get()) {
|
||||
throw new SizeLimitExceededException(
|
||||
Optional.of(path), contents.length, maxFileSize.get());
|
||||
}
|
||||
dbStore.addURLIndexForProject(projectName, getCacheKeyFromUrl(url), path);
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a suitable cache key from the given file URL.
|
||||
*
|
||||
* The file URL returned by the web service may contain a token parameter
|
||||
* used for authentication. This token changes for every request, so we
|
||||
* need to strip it from the query string before using the URL as a cache
|
||||
* key.
|
||||
*/
|
||||
private String getCacheKeyFromUrl(String url) {
|
||||
// We're not doing proper URL parsing here, but it should be enough to
|
||||
// remove the token without touching the important parts of the URL.
|
||||
//
|
||||
// The URL looks like:
|
||||
//
|
||||
// https://history.overleaf.com/api/projects/:project_id/blobs/:hash?token=:token&_path=:path
|
||||
return url.replaceAll("token=[^&]*", "token=REMOVED");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.snapshot;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.GetForVersionRequest;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.GetForVersionResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.GetSavedVersRequest;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.GetSavedVersResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PushRequest;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PushResult;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public class NetSnapshotApi implements SnapshotApi {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<GetDocResult> getDoc(
|
||||
Optional<Credential> oauth2, String projectName) {
|
||||
return new GetDocRequest(opt(oauth2), projectName).request();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<GetForVersionResult> getForVersion(
|
||||
Optional<Credential> oauth2, String projectName, int versionId) {
|
||||
return new GetForVersionRequest(
|
||||
opt(oauth2), projectName, versionId).request();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<GetSavedVersResult> getSavedVers(
|
||||
Optional<Credential> oauth2, String projectName) {
|
||||
return new GetSavedVersRequest(opt(oauth2), projectName).request();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PushResult> push(
|
||||
Optional<Credential> oauth2,
|
||||
CandidateSnapshot candidateSnapshot,
|
||||
String postbackKey
|
||||
) {
|
||||
return new PushRequest(
|
||||
opt(oauth2), candidateSnapshot, postbackKey).request();
|
||||
}
|
||||
|
||||
private static Credential opt(Optional<Credential> oauth2) {
|
||||
return oauth2.orElse(null);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.snapshot;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
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.exception.FailedConnectionException;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.GetForVersionResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.GetSavedVersResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PushResult;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface SnapshotApi {
|
||||
|
||||
CompletableFuture<GetDocResult> getDoc(
|
||||
Optional<Credential> oauth2, String projectName);
|
||||
|
||||
CompletableFuture<GetForVersionResult> getForVersion(
|
||||
Optional<Credential> oauth2, String projectName, int versionId);
|
||||
|
||||
CompletableFuture<GetSavedVersResult> getSavedVers(
|
||||
Optional<Credential> oauth2, String projectName);
|
||||
|
||||
CompletableFuture<PushResult> push(
|
||||
Optional<Credential> oauth2,
|
||||
CandidateSnapshot candidateSnapshot,
|
||||
String postbackKey);
|
||||
|
||||
static <T> T getResult(CompletableFuture<T> result)
|
||||
throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
|
||||
try {
|
||||
return result.join();
|
||||
} catch (CompletionException e) {
|
||||
try {
|
||||
throw e.getCause();
|
||||
} catch (MissingRepositoryException
|
||||
| FailedConnectionException
|
||||
| ForbiddenException
|
||||
| RuntimeException r) {
|
||||
throw r;
|
||||
} catch (Throwable __) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.snapshot;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
|
||||
import uk.ac.ic.wlgitbridge.data.model.Snapshot;
|
||||
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.exception.FailedConnectionException;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.GetForVersionResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotData;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.GetSavedVersResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.SnapshotInfo;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.PushResult;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.InvalidProjectException;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Created by winston on 02/07/2017.
|
||||
*/
|
||||
public class SnapshotApiFacade {
|
||||
|
||||
private final SnapshotApi api;
|
||||
|
||||
public SnapshotApiFacade(SnapshotApi api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public boolean projectExists(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName
|
||||
) throws FailedConnectionException, GitUserException {
|
||||
try {
|
||||
SnapshotApi
|
||||
.getResult(api.getDoc(oauth2, projectName))
|
||||
.getVersionID();
|
||||
return true;
|
||||
} catch (InvalidProjectException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<GetDocResult> getDoc(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName
|
||||
) throws FailedConnectionException, GitUserException {
|
||||
try {
|
||||
GetDocResult doc = SnapshotApi
|
||||
.getResult(api.getDoc(oauth2, projectName));
|
||||
doc.getVersionID();
|
||||
return Optional.of(doc);
|
||||
} catch (InvalidProjectException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Deque<Snapshot> getSnapshots(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
int afterVersionId
|
||||
) throws GitUserException, FailedConnectionException {
|
||||
List<SnapshotInfo> snapshotInfos = getSnapshotInfosAfterVersion(
|
||||
oauth2,
|
||||
projectName,
|
||||
afterVersionId
|
||||
);
|
||||
List<SnapshotData> snapshotDatas = getMatchingSnapshotData(
|
||||
oauth2,
|
||||
projectName,
|
||||
snapshotInfos
|
||||
);
|
||||
return combine(snapshotInfos, snapshotDatas);
|
||||
}
|
||||
|
||||
public PushResult push(
|
||||
Optional<Credential> oauth2,
|
||||
CandidateSnapshot candidateSnapshot,
|
||||
String postbackKey
|
||||
) throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
|
||||
return SnapshotApi.getResult(api.push(
|
||||
oauth2, candidateSnapshot, postbackKey));
|
||||
}
|
||||
|
||||
private List<SnapshotInfo> getSnapshotInfosAfterVersion(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
int version
|
||||
) throws FailedConnectionException, GitUserException {
|
||||
SortedSet<SnapshotInfo> versions = new TreeSet<>();
|
||||
CompletableFuture<GetDocResult> getDoc
|
||||
= api.getDoc(oauth2, projectName);
|
||||
CompletableFuture<GetSavedVersResult> savedVers
|
||||
= api.getSavedVers(oauth2, projectName);
|
||||
GetDocResult latestDoc = SnapshotApi.getResult(getDoc);
|
||||
int latest = latestDoc.getVersionID();
|
||||
// Handle edge-case for projects with no changes, that were imported
|
||||
// to v2. In which case both `latest` and `version` will be zero.
|
||||
// See: https://github.com/overleaf/writelatex-git-bridge/pull/50
|
||||
if (latest > version || (latest == 0 && version == 0)) {
|
||||
for (
|
||||
SnapshotInfo snapshotInfo :
|
||||
SnapshotApi.getResult(savedVers).getSavedVers()
|
||||
) {
|
||||
if (snapshotInfo.getVersionId() > version) {
|
||||
versions.add(snapshotInfo);
|
||||
}
|
||||
}
|
||||
versions.add(new SnapshotInfo(
|
||||
latest,
|
||||
latestDoc.getCreatedAt(),
|
||||
latestDoc.getName(),
|
||||
latestDoc.getEmail()
|
||||
));
|
||||
|
||||
}
|
||||
return new ArrayList<>(versions);
|
||||
}
|
||||
|
||||
private List<SnapshotData> getMatchingSnapshotData(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
List<SnapshotInfo> snapshotInfos
|
||||
) throws FailedConnectionException, ForbiddenException {
|
||||
List<CompletableFuture<GetForVersionResult>> firedRequests
|
||||
= fireDataRequests(oauth2, projectName, snapshotInfos);
|
||||
List<SnapshotData> snapshotDataList = new ArrayList<>();
|
||||
for (CompletableFuture<GetForVersionResult> fired : firedRequests) {
|
||||
snapshotDataList.add(fired.join().getSnapshotData());
|
||||
}
|
||||
return snapshotDataList;
|
||||
}
|
||||
|
||||
private List<CompletableFuture<GetForVersionResult>> fireDataRequests(
|
||||
Optional<Credential> oauth2,
|
||||
String projectName,
|
||||
List<SnapshotInfo> snapshotInfos
|
||||
) {
|
||||
return snapshotInfos
|
||||
.stream()
|
||||
.map(snap -> api.getForVersion(
|
||||
oauth2, projectName, snap.getVersionId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Deque<Snapshot> combine(
|
||||
List<SnapshotInfo> snapshotInfos,
|
||||
List<SnapshotData> snapshotDatas
|
||||
) {
|
||||
Deque<Snapshot> snapshots = new LinkedList<>();
|
||||
Iterator<SnapshotInfo> infos = snapshotInfos.iterator();
|
||||
Iterator<SnapshotData> datas = snapshotDatas.iterator();
|
||||
while (infos.hasNext()) {
|
||||
snapshots.add(new Snapshot(infos.next(), datas.next()));
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.job;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class NoopSwapJob implements SwapJob {
|
||||
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void stop() {}
|
||||
|
||||
@Override
|
||||
public void evict(String projName) {}
|
||||
|
||||
@Override
|
||||
public void restore(String projName) {}
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.job;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.store.SwapStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface SwapJob {
|
||||
|
||||
enum CompressionMethod { Bzip2, Gzip }
|
||||
|
||||
static CompressionMethod stringToCompressionMethod(String compressionString) {
|
||||
if (compressionString == null) {
|
||||
return null;
|
||||
}
|
||||
CompressionMethod result;
|
||||
switch (compressionString) {
|
||||
case "gzip":
|
||||
result = CompressionMethod.Gzip;
|
||||
break;
|
||||
case "bzip2":
|
||||
result = CompressionMethod.Bzip2;
|
||||
break;
|
||||
default:
|
||||
result = null;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static String compressionMethodAsString(CompressionMethod compressionMethod) {
|
||||
if (compressionMethod == null) {
|
||||
return null;
|
||||
}
|
||||
String result;
|
||||
switch (compressionMethod) {
|
||||
case Gzip:
|
||||
result = "gzip";
|
||||
break;
|
||||
case Bzip2:
|
||||
result = "bzip2";
|
||||
break;
|
||||
default:
|
||||
result = null;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static SwapJob fromConfig(
|
||||
Optional<SwapJobConfig> cfg,
|
||||
ProjectLock lock,
|
||||
RepoStore repoStore,
|
||||
DBStore dbStore,
|
||||
SwapStore swapStore
|
||||
) {
|
||||
if (cfg.isPresent()) {
|
||||
return new SwapJobImpl(
|
||||
cfg.get(),
|
||||
lock,
|
||||
repoStore,
|
||||
dbStore,
|
||||
swapStore
|
||||
);
|
||||
}
|
||||
return new NoopSwapJob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the swap job, which should schedule an attempted swap at the given
|
||||
* configured interval (config["swapJob"]["intervalMillis"]
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Stops the stop job.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Called by the swap job when a project should be evicted.
|
||||
*
|
||||
* Pre:
|
||||
* 1. projName must be in repoStore
|
||||
* 2. projName should not be in swapStore
|
||||
* 3. projName should be PRESENT in dbStore (last_accessed is not null)
|
||||
*
|
||||
* Acquires the project lock and performs an eviction of projName.
|
||||
*
|
||||
* Post:
|
||||
* 1. projName should not in repoStore
|
||||
* 2. projName must be in swapStore
|
||||
* 3. projName must be SWAPPED in dbStore (last_accessed is null)
|
||||
* @param projName
|
||||
* @throws IOException
|
||||
*/
|
||||
void evict(String projName) throws IOException;
|
||||
|
||||
/**
|
||||
* Called on a project when it must be restored.
|
||||
*
|
||||
* Pre:
|
||||
* 1. projName should not be in repoStore
|
||||
* 2. projName must be in swapStore
|
||||
* 3. projName must be SWAPPED in dbStore (last_accessed is null)
|
||||
*
|
||||
* Acquires the project lock and restores projName.
|
||||
*
|
||||
* Post:
|
||||
* 1. projName must be in repoStore
|
||||
* 2. projName should not in swapStore
|
||||
* 3. projName should be PRESENT in dbStore (last_accessed is not null)
|
||||
* @param projName
|
||||
* @throws IOException
|
||||
*/
|
||||
void restore(String projName) throws IOException;
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.job;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.job.SwapJob.CompressionMethod;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class SwapJobConfig {
|
||||
|
||||
private final int minProjects;
|
||||
private final int lowGiB;
|
||||
private final int highGiB;
|
||||
private final long intervalMillis;
|
||||
private final String compressionMethod;
|
||||
|
||||
public SwapJobConfig(
|
||||
int minProjects,
|
||||
int lowGiB,
|
||||
int highGiB,
|
||||
long intervalMillis,
|
||||
String compressionMethod
|
||||
) {
|
||||
this.minProjects = minProjects;
|
||||
this.lowGiB = lowGiB;
|
||||
this.highGiB = highGiB;
|
||||
this.intervalMillis = intervalMillis;
|
||||
this.compressionMethod = compressionMethod;
|
||||
}
|
||||
|
||||
public int getMinProjects() {
|
||||
return minProjects;
|
||||
}
|
||||
|
||||
public int getLowGiB() {
|
||||
return lowGiB;
|
||||
}
|
||||
|
||||
public int getHighGiB() {
|
||||
return highGiB;
|
||||
}
|
||||
|
||||
public long getIntervalMillis() {
|
||||
return intervalMillis;
|
||||
}
|
||||
|
||||
public SwapJob.CompressionMethod getCompressionMethod() {
|
||||
CompressionMethod result = SwapJob.stringToCompressionMethod(compressionMethod);
|
||||
if (result == null) {
|
||||
Log.info("SwapJobConfig: un-supported compressionMethod '{}', default to 'bzip2'", compressionMethod);
|
||||
result = CompressionMethod.Bzip2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.job;
|
||||
|
||||
import com.google.api.client.repackaged.com.google.common.base.Preconditions;
|
||||
import uk.ac.ic.wlgitbridge.bridge.db.DBStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.LockGuard;
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.swap.store.SwapStore;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.TimerUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public class SwapJobImpl implements SwapJob {
|
||||
|
||||
private static final long GiB = (1l << 30);
|
||||
|
||||
int minProjects;
|
||||
long lowWatermarkBytes;
|
||||
long highWatermarkBytes;
|
||||
Duration interval;
|
||||
|
||||
private final ProjectLock lock;
|
||||
private final RepoStore repoStore;
|
||||
private final DBStore dbStore;
|
||||
private final SwapStore swapStore;
|
||||
private final CompressionMethod compressionMethod;
|
||||
|
||||
private final Timer timer;
|
||||
|
||||
final AtomicInteger swaps;
|
||||
|
||||
public SwapJobImpl(
|
||||
SwapJobConfig cfg,
|
||||
ProjectLock lock,
|
||||
RepoStore repoStore,
|
||||
DBStore dbStore,
|
||||
SwapStore swapStore
|
||||
) {
|
||||
this(
|
||||
cfg.getMinProjects(),
|
||||
GiB * cfg.getLowGiB(),
|
||||
GiB * cfg.getHighGiB(),
|
||||
Duration.ofMillis(cfg.getIntervalMillis()),
|
||||
cfg.getCompressionMethod(),
|
||||
lock,
|
||||
repoStore,
|
||||
dbStore,
|
||||
swapStore
|
||||
);
|
||||
}
|
||||
|
||||
SwapJobImpl(
|
||||
int minProjects,
|
||||
long lowWatermarkBytes,
|
||||
long highWatermarkBytes,
|
||||
Duration interval,
|
||||
CompressionMethod method,
|
||||
ProjectLock lock,
|
||||
RepoStore repoStore,
|
||||
DBStore dbStore,
|
||||
SwapStore swapStore
|
||||
) {
|
||||
this.minProjects = minProjects;
|
||||
this.lowWatermarkBytes = lowWatermarkBytes;
|
||||
this.highWatermarkBytes = highWatermarkBytes;
|
||||
this.interval = interval;
|
||||
this.compressionMethod = method;
|
||||
this.lock = lock;
|
||||
this.repoStore = repoStore;
|
||||
this.dbStore = dbStore;
|
||||
this.swapStore = swapStore;
|
||||
timer = new Timer();
|
||||
swaps = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
timer.schedule(
|
||||
TimerUtils.makeTimerTask(this::doSwap),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
private void doSwap() {
|
||||
try {
|
||||
doSwap_();
|
||||
} catch (Throwable t) {
|
||||
Log.warn("Exception thrown during swap job", t);
|
||||
}
|
||||
timer.schedule(
|
||||
TimerUtils.makeTimerTask(this::doSwap),
|
||||
interval.toMillis()
|
||||
);
|
||||
}
|
||||
|
||||
private void doSwap_() {
|
||||
ArrayList<String> exceptionProjectNames = new ArrayList<String>();
|
||||
|
||||
Log.info("Running swap number {}", swaps.get() + 1);
|
||||
long totalSize = repoStore.totalSize();
|
||||
Log.info("Size is {}/{} (high)", totalSize, highWatermarkBytes);
|
||||
if (totalSize < highWatermarkBytes) {
|
||||
Log.info("No need to swap.");
|
||||
swaps.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
int numProjects = dbStore.getNumProjects();
|
||||
// while we have too many projects on disk
|
||||
while (
|
||||
(totalSize = repoStore.totalSize()) > lowWatermarkBytes &&
|
||||
(numProjects = dbStore.getNumUnswappedProjects()) > minProjects
|
||||
) {
|
||||
// check if we've had too many exceptions so far
|
||||
if (exceptionProjectNames.size() >= 20) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s: exceptionProjectNames) {
|
||||
sb.append(s);
|
||||
sb.append(' ');
|
||||
}
|
||||
Log.error(
|
||||
"Too many exceptions while running swap, giving up on this run: {}",
|
||||
sb.toString()
|
||||
);
|
||||
break;
|
||||
}
|
||||
// get the oldest project and try to swap it
|
||||
String projectName = dbStore.getOldestUnswappedProject();
|
||||
try {
|
||||
evict(projectName);
|
||||
} catch (Exception e) {
|
||||
Log.warn("[{}] Exception while swapping, mark project and move on", projectName, e);
|
||||
// NOTE: this is something of a hack. If a project fails to swap we get stuck in a
|
||||
// loop where `dbStore.getOldestUnswappedProject()` gives the same failing project over and over again,
|
||||
// which fills up the disk with errors. By touching the access time we can mark the project as a
|
||||
// non-candidate for swapping. Ideally we should be checking the logs for these log events and fixing
|
||||
// whatever is wrong with the project
|
||||
dbStore.setLastAccessedTime(
|
||||
projectName,
|
||||
Timestamp.valueOf(LocalDateTime.now())
|
||||
);
|
||||
exceptionProjectNames.add(projectName);
|
||||
}
|
||||
}
|
||||
if (totalSize > lowWatermarkBytes) {
|
||||
Log.warn(
|
||||
"Finished swapping, but total size is still too high."
|
||||
);
|
||||
}
|
||||
Log.info(
|
||||
"Size: {}/{} (low), " +
|
||||
"{} (high), " +
|
||||
"projects on disk: {}/{}, " +
|
||||
"min projects on disk: {}",
|
||||
totalSize,
|
||||
lowWatermarkBytes,
|
||||
highWatermarkBytes,
|
||||
numProjects,
|
||||
dbStore.getNumProjects(),
|
||||
minProjects
|
||||
);
|
||||
swaps.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SwapJob#evict(String) for high-level description.
|
||||
*
|
||||
* 1. Acquires the project lock.
|
||||
* 2. Gets a bz2 stream and size of a project from the repo store, or throws
|
||||
* 3. Uploads the bz2 stream and size to the projName in the swapStore.
|
||||
* 4. Sets the last accessed time in the dbStore to null, which makes our
|
||||
* state SWAPPED
|
||||
* 5. Removes the project from the repo store.
|
||||
* @param projName
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void evict(String projName) throws IOException {
|
||||
Preconditions.checkNotNull(projName, "projName was null");
|
||||
Log.info("Evicting project: {}", projName);
|
||||
try (LockGuard __ = lock.lockGuard(projName)) {
|
||||
try {
|
||||
repoStore.gcProject(projName);
|
||||
} catch (Exception e) {
|
||||
Log.error("[{}] Exception while running gc on project: {}", projName, e);
|
||||
}
|
||||
long[] sizePtr = new long[1];
|
||||
try (InputStream blob = getBlobStream(projName, sizePtr)) {
|
||||
swapStore.upload(projName, blob, sizePtr[0]);
|
||||
String compression = SwapJob.compressionMethodAsString(compressionMethod);
|
||||
if (compression == null) {
|
||||
throw new RuntimeException("invalid compression method, should not happen");
|
||||
}
|
||||
dbStore.swap(projName, compression);
|
||||
repoStore.remove(projName);
|
||||
}
|
||||
}
|
||||
Log.info("Evicted project: {}", projName);
|
||||
}
|
||||
|
||||
private InputStream getBlobStream(String projName, long[] sizePtr) throws IOException {
|
||||
if (compressionMethod == CompressionMethod.Gzip) {
|
||||
return repoStore.gzipProject(projName, sizePtr);
|
||||
} else if (compressionMethod == CompressionMethod.Bzip2) {
|
||||
return repoStore.bzip2Project(projName, sizePtr);
|
||||
} else {
|
||||
throw new RuntimeException("invalid compression method, should not happen");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SwapJob#restore(String) for high-level description.
|
||||
*
|
||||
* 1. Acquires the project lock.
|
||||
* 2. Gets a bz2 stream for the project from the swapStore.
|
||||
* 3. Fully downloads and places the bz2 stream back in the repo store.
|
||||
* 4. Sets the last accessed time in the dbStore to now, which makes our
|
||||
* state PRESENT and the last project to be evicted.
|
||||
* @param projName
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void restore(String projName) throws IOException {
|
||||
try (LockGuard __ = lock.lockGuard(projName)) {
|
||||
try (InputStream zipped = swapStore.openDownloadStream(projName)) {
|
||||
String compression = dbStore.getSwapCompression(projName);
|
||||
if (compression == null) {
|
||||
throw new RuntimeException("Missing compression method during restore, should not happen");
|
||||
}
|
||||
if ("gzip".equals(compression)) {
|
||||
repoStore.ungzipProject(
|
||||
projName,
|
||||
zipped
|
||||
);
|
||||
} else if ("bzip2".equals(compression)) {
|
||||
repoStore.unbzip2Project(
|
||||
projName,
|
||||
zipped
|
||||
);
|
||||
}
|
||||
swapStore.remove(projName);
|
||||
dbStore.restore(projName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.store;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created by winston on 23/08/2016.
|
||||
*/
|
||||
public class InMemorySwapStore implements SwapStore {
|
||||
|
||||
private final Map<String, byte[]> store;
|
||||
|
||||
public InMemorySwapStore() {
|
||||
store = new HashMap<>();
|
||||
}
|
||||
|
||||
public InMemorySwapStore(SwapStoreConfig __) {
|
||||
this();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(
|
||||
String projectName,
|
||||
InputStream uploadStream,
|
||||
long contentLength
|
||||
) throws IOException {
|
||||
store.put(
|
||||
projectName,
|
||||
IOUtils.toByteArray(uploadStream, contentLength)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openDownloadStream(String projectName) {
|
||||
byte[] buf = store.get(projectName);
|
||||
if (buf == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"no such project in swap store: " + projectName
|
||||
);
|
||||
}
|
||||
return new ByteArrayInputStream(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String projectName) {
|
||||
store.remove(projectName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.store;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class NoopSwapStore implements SwapStore {
|
||||
|
||||
public NoopSwapStore(SwapStoreConfig __) {}
|
||||
|
||||
@Override
|
||||
public void upload(
|
||||
String projectName,
|
||||
InputStream uploadStream,
|
||||
long contentLength
|
||||
) {}
|
||||
|
||||
@Override
|
||||
public InputStream openDownloadStream(String projectName) {
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String projectName) {}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.store;
|
||||
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
|
||||
import com.amazonaws.services.s3.model.*;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Created by winston on 21/08/2016.
|
||||
*/
|
||||
public class S3SwapStore implements SwapStore {
|
||||
|
||||
private final AmazonS3 s3;
|
||||
|
||||
private final String bucketName;
|
||||
|
||||
public S3SwapStore(SwapStoreConfig cfg) {
|
||||
this(
|
||||
cfg.getAwsAccessKey(),
|
||||
cfg.getAwsSecret(),
|
||||
cfg.getS3BucketName(),
|
||||
cfg.getAwsRegion()
|
||||
);
|
||||
}
|
||||
|
||||
S3SwapStore(
|
||||
String accessKey,
|
||||
String secret,
|
||||
String bucketName,
|
||||
String region
|
||||
) {
|
||||
String regionToUse = null;
|
||||
if (region == null) {
|
||||
regionToUse = "us-east-1";
|
||||
} else {
|
||||
regionToUse = region;
|
||||
}
|
||||
s3 = AmazonS3ClientBuilder
|
||||
.standard()
|
||||
.withRegion(regionToUse)
|
||||
.withCredentials(
|
||||
new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secret))
|
||||
).build();
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(
|
||||
String projectName,
|
||||
InputStream uploadStream,
|
||||
long contentLength
|
||||
) {
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(contentLength);
|
||||
PutObjectRequest put = new PutObjectRequest(
|
||||
bucketName,
|
||||
projectName,
|
||||
uploadStream,
|
||||
metadata
|
||||
);
|
||||
PutObjectResult res = s3.putObject(put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openDownloadStream(String projectName) {
|
||||
GetObjectRequest get = new GetObjectRequest(
|
||||
bucketName,
|
||||
projectName
|
||||
);
|
||||
S3Object res = s3.getObject(get);
|
||||
return res.getObjectContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String projectName) {
|
||||
DeleteObjectRequest del = new DeleteObjectRequest(
|
||||
bucketName,
|
||||
projectName
|
||||
);
|
||||
s3.deleteObject(del);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.store;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public interface SwapStore {
|
||||
|
||||
Map<String, Function<SwapStoreConfig, SwapStore>> swapStores =
|
||||
new HashMap<String, Function<SwapStoreConfig, SwapStore>>() {
|
||||
|
||||
{
|
||||
put("noop", NoopSwapStore::new);
|
||||
put("memory", InMemorySwapStore::new);
|
||||
put("s3", S3SwapStore::new);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
static SwapStore fromConfig(
|
||||
Optional<SwapStoreConfig> cfg
|
||||
) {
|
||||
SwapStoreConfig cfg_ = cfg.orElse(SwapStoreConfig.NOOP);
|
||||
String type = cfg_.getType();
|
||||
return swapStores.get(type).apply(cfg_);
|
||||
}
|
||||
|
||||
void upload(
|
||||
String projectName,
|
||||
InputStream uploadStream,
|
||||
long contentLength
|
||||
) throws IOException;
|
||||
|
||||
InputStream openDownloadStream(String projectName);
|
||||
|
||||
void remove(String projectName);
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.swap.store;
|
||||
|
||||
/**
|
||||
* Created by winston on 24/08/2016.
|
||||
*/
|
||||
public class SwapStoreConfig {
|
||||
|
||||
public static final SwapStoreConfig NOOP = new SwapStoreConfig(
|
||||
"noop",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
private String type;
|
||||
private String awsAccessKey;
|
||||
private String awsSecret;
|
||||
private String s3BucketName;
|
||||
private String awsRegion;
|
||||
|
||||
public SwapStoreConfig() {}
|
||||
|
||||
public SwapStoreConfig(
|
||||
String awsAccessKey,
|
||||
String awsSecret,
|
||||
String s3BucketName,
|
||||
String awsRegion
|
||||
) {
|
||||
this(
|
||||
"s3",
|
||||
awsAccessKey,
|
||||
awsSecret,
|
||||
s3BucketName,
|
||||
awsRegion
|
||||
);
|
||||
}
|
||||
|
||||
SwapStoreConfig(
|
||||
String type,
|
||||
String awsAccessKey,
|
||||
String awsSecret,
|
||||
String s3BucketName,
|
||||
String awsRegion
|
||||
) {
|
||||
this.type = type;
|
||||
this.awsAccessKey = awsAccessKey;
|
||||
this.awsSecret = awsSecret;
|
||||
this.s3BucketName = s3BucketName;
|
||||
this.awsRegion = awsRegion;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getAwsAccessKey() {
|
||||
return awsAccessKey;
|
||||
}
|
||||
|
||||
public String getAwsSecret() {
|
||||
return awsSecret;
|
||||
}
|
||||
|
||||
public String getS3BucketName() {
|
||||
return s3BucketName;
|
||||
}
|
||||
|
||||
public String getAwsRegion() { return awsRegion; }
|
||||
|
||||
public SwapStoreConfig sanitisedCopy() {
|
||||
return new SwapStoreConfig(
|
||||
type,
|
||||
awsAccessKey == null ? null : "<awsAccessKey>",
|
||||
awsSecret == null ? null : "<awsSecret>",
|
||||
s3BucketName,
|
||||
awsRegion
|
||||
);
|
||||
}
|
||||
|
||||
public static SwapStoreConfig sanitisedCopy(SwapStoreConfig swapStore) {
|
||||
return swapStore == null ? null : swapStore.sanitisedCopy();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package uk.ac.ic.wlgitbridge.bridge.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
/**
|
||||
* Created by winston on 01/07/2017.
|
||||
*/
|
||||
public class CastUtil {
|
||||
|
||||
public static int assumeInt(long l) {
|
||||
Preconditions.checkArgument(
|
||||
l <= (long) Integer.MAX_VALUE
|
||||
&& l >= (long) Integer.MIN_VALUE,
|
||||
l + " cannot fit inside an int");
|
||||
return (int) l;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package uk.ac.ic.wlgitbridge.data;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawFile;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* Created by Winston on 16/11/14.
|
||||
*/
|
||||
public class CandidateSnapshot implements AutoCloseable {
|
||||
|
||||
private final String projectName;
|
||||
private final int currentVersion;
|
||||
private final List<ServletFile> files;
|
||||
private final List<String> deleted;
|
||||
private File attsDirectory;
|
||||
|
||||
public CandidateSnapshot(
|
||||
String projectName,
|
||||
int currentVersion,
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents
|
||||
) {
|
||||
this.projectName = projectName;
|
||||
this.currentVersion = currentVersion;
|
||||
files = diff(directoryContents, oldDirectoryContents);
|
||||
deleted = deleted(directoryContents, oldDirectoryContents);
|
||||
}
|
||||
|
||||
private List<ServletFile> diff(
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents
|
||||
) {
|
||||
List<ServletFile> files = new LinkedList<ServletFile>();
|
||||
Map<String, RawFile> fileTable = directoryContents.getFileTable();
|
||||
Map<String, RawFile> oldFileTable = oldDirectoryContents.getFileTable();
|
||||
for (Entry<String, RawFile> entry : fileTable.entrySet()) {
|
||||
RawFile file = entry.getValue();
|
||||
files.add(new ServletFile(file, oldFileTable.get(file.getPath())));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private List<String> deleted(
|
||||
RawDirectory directoryContents,
|
||||
RawDirectory oldDirectoryContents
|
||||
) {
|
||||
List<String> deleted = new LinkedList<String>();
|
||||
Map<String, RawFile> fileTable = directoryContents.getFileTable();
|
||||
for (
|
||||
Entry<String, RawFile> entry :
|
||||
oldDirectoryContents.getFileTable().entrySet()
|
||||
) {
|
||||
String path = entry.getKey();
|
||||
RawFile newFile = fileTable.get(path);
|
||||
if (newFile == null) {
|
||||
deleted.add(path);
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void writeServletFiles(File rootGitDirectory) throws IOException {
|
||||
attsDirectory = new File(
|
||||
rootGitDirectory,
|
||||
".wlgb/atts/" + projectName
|
||||
);
|
||||
for (ServletFile file : files) {
|
||||
if (file.isChanged()) {
|
||||
file.writeToDiskWithName(attsDirectory, file.getUniqueIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteServletFiles() throws IOException {
|
||||
if (attsDirectory != null) {
|
||||
Util.deleteDirectory(attsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public JsonElement getJsonRepresentation(String postbackKey) {
|
||||
String projectURL = Util.getPostbackURL() + "api/" + projectName;
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("latestVerId", currentVersion);
|
||||
jsonObject.add("files", getFilesAsJson(projectURL, postbackKey));
|
||||
jsonObject.addProperty(
|
||||
"postbackUrl", projectURL + "/" + postbackKey + "/postback"
|
||||
);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
private JsonArray getFilesAsJson(String projectURL, String postbackKey) {
|
||||
JsonArray filesArray = new JsonArray();
|
||||
for (ServletFile file : files) {
|
||||
filesArray.add(getFileAsJson(file, projectURL, postbackKey));
|
||||
}
|
||||
return filesArray;
|
||||
}
|
||||
|
||||
private JsonObject getFileAsJson(
|
||||
ServletFile file,
|
||||
String projectURL,
|
||||
String postbackKey
|
||||
) {
|
||||
JsonObject jsonFile = new JsonObject();
|
||||
jsonFile.addProperty("name", file.getPath());
|
||||
if (file.isChanged()) {
|
||||
String identifier = file.getUniqueIdentifier();
|
||||
String url = projectURL + "/" + identifier + "?key=" + postbackKey;
|
||||
jsonFile.addProperty("url", url);
|
||||
}
|
||||
return jsonFile;
|
||||
}
|
||||
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public List<String> getDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("VersionId: ");
|
||||
sb.append(currentVersion);
|
||||
sb.append(", files: ");
|
||||
sb.append(files);
|
||||
sb.append(", deleted: ");
|
||||
sb.append(deleted);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
deleteServletFiles();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package uk.ac.ic.wlgitbridge.data;
|
||||
|
||||
/**
|
||||
* Created by Winston on 21/02/15.
|
||||
*/
|
||||
public interface LockAllWaiter {
|
||||
|
||||
void threadsRemaining(int threads);
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package uk.ac.ic.wlgitbridge.data;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
/**
|
||||
* Created by Winston on 20/11/14.
|
||||
*/
|
||||
public class ProjectLockImpl implements ProjectLock {
|
||||
|
||||
private final Map<String, Lock> projectLocks;
|
||||
private final ReentrantReadWriteLock rwlock;
|
||||
private final Lock rlock;
|
||||
private final ReentrantReadWriteLock.WriteLock wlock;
|
||||
private LockAllWaiter waiter;
|
||||
private boolean waiting;
|
||||
|
||||
public ProjectLockImpl() {
|
||||
projectLocks = new HashMap<String, Lock>();
|
||||
rwlock = new ReentrantReadWriteLock();
|
||||
rlock = rwlock.readLock();
|
||||
wlock = rwlock.writeLock();
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
public ProjectLockImpl(LockAllWaiter waiter) {
|
||||
this();
|
||||
setWaiter(waiter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lockForProject(String projectName) {
|
||||
Log.debug("[{}] taking project lock", projectName);
|
||||
getLockForProjectName(projectName).lock();
|
||||
Log.debug("[{}] taking reentrant lock", projectName);
|
||||
rlock.lock();
|
||||
Log.debug("[{}] taken locks", projectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlockForProject(String projectName) {
|
||||
Log.debug("[{}] releasing project lock", projectName);
|
||||
getLockForProjectName(projectName).unlock();
|
||||
Log.debug("[{}] releasing reentrant lock", projectName);
|
||||
rlock.unlock();
|
||||
Log.debug("[{}] released locks", projectName);
|
||||
if (waiting) {
|
||||
Log.debug("[{}] waiting for remaining threads", projectName);
|
||||
trySignal();
|
||||
}
|
||||
}
|
||||
|
||||
private void trySignal() {
|
||||
int threads = rwlock.getReadLockCount();
|
||||
Log.debug("-> waiting for {} threads", threads);
|
||||
if (waiter != null && threads > 0) {
|
||||
waiter.threadsRemaining(threads);
|
||||
}
|
||||
Log.debug("-> finished waiting for threads");
|
||||
}
|
||||
|
||||
public void lockAll() {
|
||||
Log.debug("-> locking all threads");
|
||||
waiting = true;
|
||||
trySignal();
|
||||
Log.debug("-> locking reentrant write lock");
|
||||
wlock.lock();
|
||||
}
|
||||
|
||||
private synchronized Lock getLockForProjectName(String projectName) {
|
||||
Lock lock = projectLocks.get(projectName);
|
||||
if (lock == null) {
|
||||
lock = new ReentrantLock();
|
||||
projectLocks.put(projectName, lock);
|
||||
}
|
||||
return lock;
|
||||
}
|
||||
|
||||
public void setWaiter(LockAllWaiter waiter) {
|
||||
this.waiter = waiter;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package uk.ac.ic.wlgitbridge.data;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawFile;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Created by Winston on 21/02/15.
|
||||
*/
|
||||
public class ServletFile extends RawFile {
|
||||
|
||||
private final RawFile file;
|
||||
private final boolean changed;
|
||||
private String uuid;
|
||||
|
||||
public ServletFile(RawFile file, RawFile oldFile) {
|
||||
this.file = file;
|
||||
this.uuid = UUID.randomUUID().toString();
|
||||
changed = !equals(oldFile);
|
||||
}
|
||||
|
||||
public String getUniqueIdentifier() { return uuid; }
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return file.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContents() {
|
||||
return file.getContents();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return getContents().length;
|
||||
}
|
||||
|
||||
public boolean isChanged() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPath();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package uk.ac.ic.wlgitbridge.data.filestore;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.data.model.Snapshot;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Winston on 14/11/14.
|
||||
*/
|
||||
public class GitDirectoryContents {
|
||||
|
||||
private final List<RawFile> files;
|
||||
private final File gitDirectory;
|
||||
private final String userName;
|
||||
private final String userEmail;
|
||||
private final String commitMessage;
|
||||
private final Date when;
|
||||
|
||||
public GitDirectoryContents(
|
||||
List<RawFile> files,
|
||||
File rootGitDirectory,
|
||||
String projectName,
|
||||
String userName,
|
||||
String userEmail,
|
||||
String commitMessage,
|
||||
Date when
|
||||
) {
|
||||
this.files = files;
|
||||
this.gitDirectory = new File(rootGitDirectory, projectName);
|
||||
this.userName = userName;
|
||||
this.userEmail = userEmail;
|
||||
this.commitMessage = commitMessage;
|
||||
this.when = when;
|
||||
}
|
||||
|
||||
public GitDirectoryContents(
|
||||
List<RawFile> files,
|
||||
File rootGitDirectory,
|
||||
String projectName,
|
||||
Snapshot snapshot
|
||||
) {
|
||||
this(
|
||||
files,
|
||||
rootGitDirectory,
|
||||
projectName,
|
||||
snapshot.getUserName(),
|
||||
snapshot.getUserEmail(),
|
||||
snapshot.getComment(),
|
||||
snapshot.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public void write() throws IOException {
|
||||
Util.deleteInDirectoryApartFrom(gitDirectory, ".git");
|
||||
for (RawFile fileNode : files) {
|
||||
fileNode.writeToDisk(gitDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public File getDirectory() {
|
||||
return gitDirectory;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public String getUserEmail() {
|
||||
return userEmail;
|
||||
}
|
||||
|
||||
public String getCommitMessage() {
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
public Date getWhen() {
|
||||
return when;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package uk.ac.ic.wlgitbridge.data.filestore;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by Winston on 16/11/14.
|
||||
*/
|
||||
public class RawDirectory {
|
||||
|
||||
private final Map<String, RawFile> fileTable;
|
||||
|
||||
public RawDirectory(Map<String, RawFile> fileTable) {
|
||||
this.fileTable = fileTable;
|
||||
}
|
||||
|
||||
public Map<String, RawFile> getFileTable() {
|
||||
return fileTable;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package uk.ac.ic.wlgitbridge.data.filestore;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by Winston on 16/11/14.
|
||||
*/
|
||||
public abstract class RawFile {
|
||||
|
||||
public abstract String getPath();
|
||||
|
||||
public abstract byte[] getContents();
|
||||
|
||||
public abstract long size();
|
||||
|
||||
public final void writeToDisk(File directory) throws IOException {
|
||||
writeToDiskWithName(directory, getPath());
|
||||
}
|
||||
|
||||
public final void writeToDiskWithName(File directory, String name) throws IOException {
|
||||
File file = new File(directory, name);
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
OutputStream out = new FileOutputStream(file);
|
||||
out.write(getContents());
|
||||
out.close();
|
||||
Log.info("Wrote file: {}", file.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof RawFile)) {
|
||||
return false;
|
||||
}
|
||||
RawFile that = (RawFile) obj;
|
||||
return getPath().equals(that.getPath())
|
||||
&& Arrays.equals(getContents(), that.getContents());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package uk.ac.ic.wlgitbridge.data.filestore;
|
||||
|
||||
/**
|
||||
* Created by Winston on 16/11/14.
|
||||
*/
|
||||
public class RepositoryFile extends RawFile {
|
||||
|
||||
private final String path;
|
||||
private final byte[] contents;
|
||||
|
||||
public RepositoryFile(String path, byte[] contents) {
|
||||
this.path = path;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContents() {
|
||||
return contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return contents.length;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package uk.ac.ic.wlgitbridge.data.model;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotData;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotFile;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.SnapshotInfo;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.getsavedvers.WLUser;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Winston on 03/11/14.
|
||||
*/
|
||||
public class Snapshot implements Comparable<Snapshot> {
|
||||
|
||||
private final int versionID;
|
||||
private final String comment;
|
||||
private final String userName;
|
||||
private final String userEmail;
|
||||
private final Date createdAt;
|
||||
|
||||
private final List<SnapshotFile> srcs;
|
||||
private final List<SnapshotAttachment> atts;
|
||||
|
||||
public Snapshot(SnapshotInfo info, SnapshotData data) {
|
||||
versionID = info.getVersionId();
|
||||
comment = info.getComment();
|
||||
WLUser user = info.getUser();
|
||||
userName = user.getName();
|
||||
userEmail = user.getEmail();
|
||||
createdAt = new DateTime(info.getCreatedAt()).toDate();
|
||||
|
||||
srcs = data.getSrcs();
|
||||
atts = data.getAtts();
|
||||
}
|
||||
|
||||
public int getVersionID() {
|
||||
return versionID;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public String getUserEmail() {
|
||||
return userEmail;
|
||||
}
|
||||
|
||||
public List<SnapshotFile> getSrcs() {
|
||||
return srcs;
|
||||
}
|
||||
|
||||
public List<SnapshotAttachment> getAtts() {
|
||||
return atts;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Snapshot snapshot) {
|
||||
return Integer.compare(versionID, snapshot.versionID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(versionID);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package uk.ac.ic.wlgitbridge.git.exception;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FileLimitExceededException extends GitUserException {
|
||||
|
||||
private final long numFiles;
|
||||
|
||||
private final long maxFiles;
|
||||
|
||||
public FileLimitExceededException(long numFiles, long maxFiles) {
|
||||
this.numFiles = numFiles;
|
||||
this.maxFiles = maxFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "too many files";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDescriptionLines() {
|
||||
return Arrays.asList(
|
||||
"repository contains " +
|
||||
numFiles + " files, which exceeds the limit of " +
|
||||
maxFiles + " files"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package uk.ac.ic.wlgitbridge.git.exception;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public abstract class GitUserException extends Exception {
|
||||
|
||||
public abstract String getMessage();
|
||||
|
||||
public abstract List<String> getDescriptionLines();
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package uk.ac.ic.wlgitbridge.git.exception;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class InvalidGitRepository extends GitUserException {
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "invalid git repo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDescriptionLines() {
|
||||
return Arrays.asList(
|
||||
"Your Git repository contains a reference we cannot resolve.",
|
||||
"If your project contains a Git submodule,",
|
||||
"please remove it and try again."
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package uk.ac.ic.wlgitbridge.git.exception;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SizeLimitExceededException extends GitUserException {
|
||||
|
||||
private final Optional<String> path;
|
||||
|
||||
private final long actualSize;
|
||||
|
||||
private final long maxSize;
|
||||
|
||||
public SizeLimitExceededException(
|
||||
Optional<String> path, long actualSize, long maxSize) {
|
||||
this.path = path;
|
||||
this.actualSize = actualSize;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "file too big";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDescriptionLines() {
|
||||
String filename =
|
||||
path.isPresent() ? "File '" + path.get() + "' is" : "There's a file";
|
||||
return Arrays.asList(
|
||||
filename + " too large to push to "
|
||||
+ Util.getServiceName() + " via git",
|
||||
"the recommended maximum file size is 50 MiB"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package uk.ac.ic.wlgitbridge.git.exception;
|
||||
|
||||
import uk.ac.ic.wlgitbridge.snapshot.base.JSONSource;
|
||||
|
||||
/**
|
||||
* Created by winston on 20/08/2016.
|
||||
*/
|
||||
public abstract class SnapshotAPIException
|
||||
extends GitUserException
|
||||
implements JSONSource {}
|
|
@ -0,0 +1,81 @@
|
|||
package uk.ac.ic.wlgitbridge.git.handler;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ReceivePack;
|
||||
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
|
||||
import uk.ac.ic.wlgitbridge.bridge.Bridge;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore;
|
||||
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook;
|
||||
import uk.ac.ic.wlgitbridge.git.servlet.WLGitServlet;
|
||||
import uk.ac.ic.wlgitbridge.server.Oauth2Filter;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by Winston on 02/11/14.
|
||||
*/
|
||||
/**
|
||||
* One of the "big three" interfaces created by {@link WLGitServlet} to handle
|
||||
* user Git requests.
|
||||
*
|
||||
* This class just puts a {@link WriteLatexPutHook} into the {@link ReceivePack}
|
||||
* that it returns.
|
||||
*/
|
||||
public class WLReceivePackFactory
|
||||
implements ReceivePackFactory<HttpServletRequest> {
|
||||
|
||||
private final RepoStore repoStore;
|
||||
|
||||
private final Bridge bridge;
|
||||
|
||||
public WLReceivePackFactory(RepoStore repoStore, Bridge bridge) {
|
||||
this.repoStore = repoStore;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a {@link WriteLatexPutHook} into the returned {@link ReceivePack}.
|
||||
*
|
||||
* The {@link WriteLatexPutHook} needs our hostname, which we get from the
|
||||
* original {@link HttpServletRequest}, used to provide a postback URL to
|
||||
* the {@link SnapshotApi}. We also give it the oauth2 that we injected in
|
||||
* the {@link Oauth2Filter}, and the {@link Bridge}.
|
||||
*
|
||||
* At this point, the repository will have been synced to the latest on
|
||||
* Overleaf, but it's possible that an update happens on Overleaf while our
|
||||
* put hook is running. In this case, we fail, and the user tries again,
|
||||
* triggering another sync, and so on.
|
||||
* @param httpServletRequest the original request
|
||||
* @param repository the JGit {@link Repository} provided by
|
||||
* {@link WLRepositoryResolver}
|
||||
* @return a correctly hooked {@link ReceivePack}
|
||||
*/
|
||||
@Override
|
||||
public ReceivePack create(
|
||||
HttpServletRequest httpServletRequest,
|
||||
Repository repository
|
||||
) {
|
||||
Log.info(
|
||||
"[{}] Creating receive-pack",
|
||||
repository.getWorkTree().getName()
|
||||
);
|
||||
Optional<Credential> oauth2 = Optional.ofNullable(
|
||||
(Credential) httpServletRequest.getAttribute(
|
||||
Oauth2Filter.ATTRIBUTE_KEY));
|
||||
ReceivePack receivePack = new ReceivePack(repository);
|
||||
String hostname = Util.getPostbackURL();
|
||||
if (hostname == null) {
|
||||
hostname = httpServletRequest.getLocalName();
|
||||
}
|
||||
receivePack.setPreReceiveHook(
|
||||
new WriteLatexPutHook(repoStore, bridge, hostname, oauth2)
|
||||
);
|
||||
return receivePack;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package uk.ac.ic.wlgitbridge.git.handler;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ServiceMayNotContinueException;
|
||||
import org.eclipse.jgit.transport.resolver.RepositoryResolver;
|
||||
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
|
||||
import uk.ac.ic.wlgitbridge.bridge.Bridge;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook;
|
||||
import uk.ac.ic.wlgitbridge.git.servlet.WLGitServlet;
|
||||
import uk.ac.ic.wlgitbridge.server.GitBridgeServer;
|
||||
import uk.ac.ic.wlgitbridge.server.Oauth2Filter;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by Winston on 02/11/14.
|
||||
*/
|
||||
/**
|
||||
* One of the "big three" interfaces created by {@link WLGitServlet} to handle
|
||||
* user Git requests.
|
||||
*
|
||||
* This class is used by all Git requests to resolve a project name to a
|
||||
* JGit {@link Repository}, or fail by throwing an exception.
|
||||
*
|
||||
* It has a single method, {@link #open(HttpServletRequest, String)}, which
|
||||
* calls into the {@link Bridge} to synchronise the project with Overleaf, i.e.
|
||||
* bringing it onto disk and applying commits to it until it is up-to-date with
|
||||
* Overleaf.
|
||||
*/
|
||||
public class WLRepositoryResolver
|
||||
implements RepositoryResolver<HttpServletRequest> {
|
||||
|
||||
private final Bridge bridge;
|
||||
|
||||
public WLRepositoryResolver(Bridge bridge) {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls into the Bridge to resolve a project name to a JGit
|
||||
* {@link Repository}, or throw an exception.
|
||||
*
|
||||
* On success, the repository will have been brought onto disk and updated
|
||||
* to the latest (synced).
|
||||
*
|
||||
* In the case of clones and fetches, upload packs are created from the
|
||||
* returned JGit {@link Repository} by the {@link WLUploadPackFactory}.
|
||||
*
|
||||
* The project lock is acquired for this process so it can't be swapped out.
|
||||
*
|
||||
* However, it can still be swapped out between this and a Git push. The
|
||||
* push would fail due to the project changed on Overleaf between the sync
|
||||
* and the actual push to Overleaf (performed by the
|
||||
* {@link WLReceivePackFactory} and {@link WriteLatexPutHook}. In this case,
|
||||
* the user will have to try again (which prompts another update, etc. until
|
||||
* this no longer happens).
|
||||
* @param httpServletRequest The HttpServletRequest as required by the
|
||||
* interface. We injected the oauth2 creds into it with
|
||||
* {@link Oauth2Filter}, which was set up by the {@link GitBridgeServer}.
|
||||
* @param name The name of the project
|
||||
* @return the JGit {@link Repository}.
|
||||
* @throws RepositoryNotFoundException If the project does not exist
|
||||
* @throws ServiceNotAuthorizedException If the user did not auth when
|
||||
* required to
|
||||
* @throws ServiceMayNotContinueException If any other general user
|
||||
* exception occurs that must be propogated back to the user, e.g.
|
||||
* internal errors (IOException, etc), too large file, and so on.
|
||||
*/
|
||||
@Override
|
||||
public Repository open(
|
||||
HttpServletRequest httpServletRequest,
|
||||
String name
|
||||
) throws RepositoryNotFoundException,
|
||||
ServiceNotAuthorizedException,
|
||||
ServiceMayNotContinueException {
|
||||
Log.info("[{}] Request to open git repo", name);
|
||||
Optional<Credential> oauth2 = Optional.ofNullable(
|
||||
(Credential) httpServletRequest.getAttribute(
|
||||
Oauth2Filter.ATTRIBUTE_KEY));
|
||||
String projName = Util.removeAllSuffixes(name, "/", ".git");
|
||||
try {
|
||||
return bridge.getUpdatedRepo(oauth2, projName).getJGitRepository();
|
||||
} catch (RepositoryNotFoundException e) {
|
||||
Log.info("Repository not found: " + name);
|
||||
throw e;
|
||||
/*
|
||||
} catch (ServiceNotAuthorizedException e) {
|
||||
cannot occur
|
||||
} catch (ServiceNotEnabledException e) {
|
||||
cannot occur
|
||||
*/
|
||||
} catch (ServiceMayNotContinueException e) {
|
||||
/* Such as FailedConnectionException */
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
Log.warn(
|
||||
"Runtime exception when trying to open repo: " + projName,
|
||||
e
|
||||
);
|
||||
throw new ServiceMayNotContinueException(e);
|
||||
} catch (ForbiddenException e) {
|
||||
throw new ServiceNotAuthorizedException();
|
||||
} catch (GitUserException e) {
|
||||
throw new ServiceMayNotContinueException(
|
||||
e.getMessage() + "\n" +
|
||||
String.join("\n", e.getDescriptionLines()),
|
||||
e);
|
||||
} catch (IOException e) {
|
||||
Log.warn(
|
||||
"IOException when trying to open repo: " + projName,
|
||||
e
|
||||
);
|
||||
throw new ServiceMayNotContinueException("Internal server error.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package uk.ac.ic.wlgitbridge.git.handler;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.UploadPack;
|
||||
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
|
||||
import uk.ac.ic.wlgitbridge.git.servlet.WLGitServlet;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Created by Winston on 02/11/14.
|
||||
*/
|
||||
/**
|
||||
* One of the "big three" interfaces created by {@link WLGitServlet} to handle
|
||||
* user Git requests.
|
||||
*
|
||||
* The actual class doesn't do much, and most of the work is done when the
|
||||
* project name is being resolved by the {@link WLRepositoryResolver}.
|
||||
*/
|
||||
public class WLUploadPackFactory
|
||||
implements UploadPackFactory<HttpServletRequest> {
|
||||
|
||||
/**
|
||||
* This does nothing special. Synchronising the project with Overleaf will
|
||||
* have been performed by {@link WLRepositoryResolver}.
|
||||
* @param __ Not used, required by the {@link UploadPackFactory} interface
|
||||
* @param repository The JGit repository provided by the
|
||||
* {@link WLRepositoryResolver}
|
||||
* @return the {@link UploadPack}, used by JGit to serve the request
|
||||
*/
|
||||
@Override
|
||||
public UploadPack create(
|
||||
HttpServletRequest __,
|
||||
Repository repository
|
||||
) {
|
||||
Log.info(
|
||||
"[{}] Creating upload-pack",
|
||||
repository.getWorkTree().getName()
|
||||
);
|
||||
return new UploadPack(repository);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package uk.ac.ic.wlgitbridge.git.handler.hook;
|
||||
|
||||
import com.google.api.client.auth.oauth2.Credential;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.PreReceiveHook;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand.Result;
|
||||
import org.eclipse.jgit.transport.ReceivePack;
|
||||
import uk.ac.ic.wlgitbridge.bridge.Bridge;
|
||||
import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore;
|
||||
import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory;
|
||||
import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.WLReceivePackFactory;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.hook.exception.ForcedPushException;
|
||||
import uk.ac.ic.wlgitbridge.git.handler.hook.exception.WrongBranchException;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.InternalErrorException;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.OutOfDateException;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.SnapshotPostException;
|
||||
import uk.ac.ic.wlgitbridge.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by Winston on 03/11/14.
|
||||
*/
|
||||
/**
|
||||
* Created by {@link WLReceivePackFactory} to update the {@link Bridge} for a
|
||||
* user's Git push request, or fail with an error. The hook is able to approve
|
||||
* or reject a request.
|
||||
*/
|
||||
public class WriteLatexPutHook implements PreReceiveHook {
|
||||
|
||||
private final RepoStore repoStore;
|
||||
|
||||
private final Bridge bridge;
|
||||
private final String hostname;
|
||||
private final Optional<Credential> oauth2;
|
||||
|
||||
/**
|
||||
* The constructor to use, which provides the hook with the {@link Bridge},
|
||||
* the hostname (used to construct a URL to give to Overleaf to postback),
|
||||
* and the oauth2 (used to authenticate with the Snapshot API).
|
||||
* @param repoStore
|
||||
* @param bridge the {@link Bridge}
|
||||
* @param hostname the hostname used for postback from the Snapshot API
|
||||
* @param oauth2 used to authenticate with the snapshot API, or null
|
||||
*/
|
||||
public WriteLatexPutHook(
|
||||
RepoStore repoStore,
|
||||
Bridge bridge,
|
||||
String hostname,
|
||||
Optional<Credential> oauth2
|
||||
) {
|
||||
this.repoStore = repoStore;
|
||||
this.bridge = bridge;
|
||||
this.hostname = hostname;
|
||||
this.oauth2 = oauth2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreReceive(
|
||||
ReceivePack receivePack,
|
||||
Collection<ReceiveCommand> receiveCommands
|
||||
) {
|
||||
Log.debug("-> Handling {} commands in {}", receiveCommands.size(), receivePack.getRepository().getDirectory().getAbsolutePath());
|
||||
for (ReceiveCommand receiveCommand : receiveCommands) {
|
||||
try {
|
||||
handleReceiveCommand(
|
||||
oauth2,
|
||||
receivePack.getRepository(),
|
||||
receiveCommand
|
||||
);
|
||||
} catch (IOException e) {
|
||||
Log.debug("IOException on pre receive: {}", e.getMessage());
|
||||
receivePack.sendError(e.getMessage());
|
||||
receiveCommand.setResult(
|
||||
Result.REJECTED_OTHER_REASON,
|
||||
e.getMessage()
|
||||
);
|
||||
} catch (OutOfDateException e) {
|
||||
Log.debug("OutOfDateException on pre receive: {}", e.getMessage());
|
||||
receiveCommand.setResult(Result.REJECTED_NONFASTFORWARD);
|
||||
} catch (GitUserException e) {
|
||||
Log.debug("GitUserException on pre receive: {}", e.getMessage());
|
||||
handleSnapshotPostException(receivePack, receiveCommand, e);
|
||||
} catch (Throwable t) {
|
||||
Log.warn("Throwable on pre receive: {}", t.getMessage());
|
||||
handleSnapshotPostException(
|
||||
receivePack,
|
||||
receiveCommand,
|
||||
new InternalErrorException()
|
||||
);
|
||||
}
|
||||
}
|
||||
Log.debug("-> Handled {} commands in {}", receiveCommands.size(), receivePack.getRepository().getDirectory().getAbsolutePath());
|
||||
}
|
||||
|
||||
private void handleSnapshotPostException(
|
||||
ReceivePack receivePack,
|
||||
ReceiveCommand receiveCommand,
|
||||
GitUserException e
|
||||
) {
|
||||
String message = e.getMessage();
|
||||
receivePack.sendError(message);
|
||||
StringBuilder msg = new StringBuilder();
|
||||
for (
|
||||
Iterator<String> it = e.getDescriptionLines().iterator();
|
||||
it.hasNext();
|
||||
) {
|
||||
String line = it.next();
|
||||
msg.append("hint: ");
|
||||
msg.append(line);
|
||||
if (it.hasNext()) {
|
||||
msg.append('\n');
|
||||
}
|
||||
}
|
||||
receivePack.sendMessage("");
|
||||
receivePack.sendMessage(msg.toString());
|
||||
receiveCommand.setResult(Result.REJECTED_OTHER_REASON, message);
|
||||
}
|
||||
|
||||
private void handleReceiveCommand(
|
||||
Optional<Credential> oauth2,
|
||||
Repository repository,
|
||||
ReceiveCommand receiveCommand
|
||||
) throws IOException, GitUserException {
|
||||
checkBranch(receiveCommand);
|
||||
checkForcedPush(receiveCommand);
|
||||
bridge.push(
|
||||
oauth2,
|
||||
repository.getWorkTree().getName(),
|
||||
getPushedDirectoryContents(repository,
|
||||
receiveCommand),
|
||||
getOldDirectoryContents(repository),
|
||||
hostname
|
||||
);
|
||||
}
|
||||
|
||||
private void checkBranch(
|
||||
ReceiveCommand receiveCommand
|
||||
) throws WrongBranchException {
|
||||
if (!receiveCommand.getRefName().equals("refs/heads/master")) {
|
||||
throw new WrongBranchException();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForcedPush(
|
||||
ReceiveCommand receiveCommand
|
||||
) throws ForcedPushException {
|
||||
if (
|
||||
receiveCommand.getType()
|
||||
== ReceiveCommand.Type.UPDATE_NONFASTFORWARD
|
||||
) {
|
||||
throw new ForcedPushException();
|
||||
}
|
||||
}
|
||||
|
||||
private RawDirectory getPushedDirectoryContents(
|
||||
Repository repository,
|
||||
ReceiveCommand receiveCommand
|
||||
) throws IOException, GitUserException {
|
||||
return repoStore
|
||||
.useJGitRepo(repository, receiveCommand.getNewId())
|
||||
.getDirectory();
|
||||
}
|
||||
|
||||
private RawDirectory getOldDirectoryContents(
|
||||
Repository repository
|
||||
) throws IOException, GitUserException {
|
||||
return repoStore
|
||||
.useJGitRepo(repository, repository.resolve("HEAD"))
|
||||
.getDirectory();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package uk.ac.ic.wlgitbridge.git.handler.hook.exception;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import uk.ac.ic.wlgitbridge.util.Util;
|
||||
import uk.ac.ic.wlgitbridge.snapshot.push.exception.SnapshotPostException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Winston on 16/11/14.
|
||||
*/
|
||||
public class ForcedPushException extends SnapshotPostException {
|
||||
|
||||
private static final String[] DESCRIPTION_LINES = {
|
||||
"You can't git push --force to a "
|
||||
+ Util.getServiceName()
|
||||
+ " project.",
|
||||
"Try to put your changes on top of the current head.",
|
||||
"If everything else fails, delete and reclone your repository, "
|
||||
+ "make your changes, then push again."
|
||||
};
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "forced push prohibited";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDescriptionLines() {
|
||||
return Arrays.asList(DESCRIPTION_LINES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromJSON(JsonElement json) {}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue