├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .idea
└── codeStyleSettings.xml
├── LICENSE
├── README.adoc
├── build.gradle.kts
├── gitlfs-client
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── client
│ │ ├── AuthHelper.kt
│ │ ├── BatchDownloader.kt
│ │ ├── BatchSettings.kt
│ │ ├── BatchUploader.kt
│ │ ├── Client.kt
│ │ ├── HttpExecutor.kt
│ │ ├── auth
│ │ ├── AuthProvider.kt
│ │ ├── BasicAuthProvider.kt
│ │ ├── CachedAuthProvider.kt
│ │ └── ExternalAuthProvider.kt
│ │ ├── exceptions
│ │ ├── ForbiddenException.kt
│ │ ├── RequestException.kt
│ │ └── UnauthorizedException.kt
│ │ ├── internal
│ │ ├── BatchWorker.kt
│ │ ├── HttpClientExecutor.kt
│ │ ├── JsonPost.kt
│ │ ├── LfsRequest.kt
│ │ ├── LockCreate.kt
│ │ ├── LockDelete.kt
│ │ ├── LocksList.kt
│ │ ├── MetaGet.kt
│ │ ├── MetaPost.kt
│ │ ├── ObjectGet.kt
│ │ ├── ObjectPut.kt
│ │ ├── ObjectVerify.kt
│ │ ├── Request.kt
│ │ └── Work.kt
│ │ └── io
│ │ ├── ByteArrayStreamProvider.kt
│ │ ├── FileStreamProvider.kt
│ │ ├── StreamHandler.kt
│ │ ├── StreamProvider.kt
│ │ ├── StringStreamProvider.kt
│ │ └── UrlStreamProvider.kt
│ └── test
│ ├── kotlin
│ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── client
│ │ ├── AuthHelperTest.kt
│ │ ├── ClientBatchTest.kt
│ │ ├── ClientLegacyTest.kt
│ │ ├── ClientLocksTest.kt
│ │ ├── Examples.kt
│ │ ├── FakeAuthProvider.kt
│ │ ├── HttpRecord.kt
│ │ ├── HttpRecorder.kt
│ │ ├── HttpReplay.kt
│ │ ├── Recorder.kt
│ │ ├── YamlConstructor.kt
│ │ ├── YamlHelper.kt
│ │ └── YamlRepresenter.kt
│ └── resources
│ └── ru
│ └── bozaro
│ └── gitlfs
│ └── client
│ ├── batch-download-01.yml
│ ├── batch-download-02.yml
│ ├── batch-upload-01.yml
│ ├── batch-upload-02.yml
│ ├── batch-upload-chunked.yml
│ ├── legacy-download-01.yml
│ ├── legacy-download-02.yml
│ ├── legacy-upload-01.yml
│ ├── legacy-upload-02.yml
│ ├── legacy-upload-03.yml
│ ├── legacy-upload-04.yml
│ └── locking-01.yml
├── gitlfs-common
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── common
│ │ ├── Constants.kt
│ │ ├── JsonHelper.kt
│ │ ├── LockConflictException.kt
│ │ ├── VerifyLocksResult.kt
│ │ ├── data
│ │ ├── BatchItem.kt
│ │ ├── BatchReq.kt
│ │ ├── BatchRes.kt
│ │ ├── CreateLockReq.kt
│ │ ├── CreateLockRes.kt
│ │ ├── DeleteLockReq.kt
│ │ ├── DeleteLockRes.kt
│ │ ├── Error.kt
│ │ ├── Link.kt
│ │ ├── LinkType.kt
│ │ ├── Links.kt
│ │ ├── Lock.kt
│ │ ├── LockConflictRes.kt
│ │ ├── LocksRes.kt
│ │ ├── Meta.kt
│ │ ├── ObjectRes.kt
│ │ ├── Operation.kt
│ │ ├── Ref.kt
│ │ ├── User.kt
│ │ ├── VerifyLocksReq.kt
│ │ └── VerifyLocksRes.kt
│ │ └── io
│ │ └── InputStreamValidator.kt
│ └── test
│ ├── kotlin
│ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── common
│ │ └── data
│ │ ├── BatchReqTest.kt
│ │ ├── BatchResTest.kt
│ │ ├── CreateLockReqTest.kt
│ │ ├── CreateLockResTest.kt
│ │ ├── DateTest.kt
│ │ ├── DeleteLockReqTest.kt
│ │ ├── LinkTest.kt
│ │ ├── LocksResTest.kt
│ │ ├── MetaTest.kt
│ │ ├── ObjectResTest.kt
│ │ ├── SerializeTester.kt
│ │ ├── VerifyLocksReqTest.kt
│ │ └── VerifyLocksResTest.kt
│ └── resources
│ └── ru
│ └── bozaro
│ └── gitlfs
│ └── common
│ └── data
│ ├── batch-req-01.json
│ ├── batch-res-01.json
│ ├── create-lock-req-01.json
│ ├── create-lock-res-01.json
│ ├── delete-lock-req-01.json
│ ├── link-01.json
│ ├── link-02.json
│ ├── locks-res-01.json
│ ├── meta-01.json
│ ├── object-res-01.json
│ ├── object-res-02.json
│ ├── verify-locks-req-01.json
│ └── verify-locks-res-01.json
├── gitlfs-pointer
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── pointer
│ │ ├── Constants.kt
│ │ └── Pointer.kt
│ └── test
│ ├── kotlin
│ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── pointer
│ │ └── PointerTest.kt
│ └── resources
│ └── ru
│ └── bozaro
│ └── gitlfs
│ └── pointer
│ ├── pointer-invalid-01.dat
│ ├── pointer-invalid-02.dat
│ ├── pointer-invalid-03.dat
│ ├── pointer-invalid-04.dat
│ ├── pointer-invalid-05.dat
│ ├── pointer-invalid-06.dat
│ ├── pointer-invalid-07.dat
│ ├── pointer-invalid-08.dat
│ ├── pointer-invalid-09.dat
│ ├── pointer-invalid-10.dat
│ ├── pointer-valid-01.dat
│ ├── pointer-valid-02.dat
│ └── pointer-valid-03.dat
├── gitlfs-server
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── ru
│ │ └── bozaro
│ │ └── gitlfs
│ │ └── server
│ │ ├── ContentManager.kt
│ │ ├── ContentServlet.kt
│ │ ├── ForbiddenError.kt
│ │ ├── LocalPointerManager.kt
│ │ ├── LockManager.kt
│ │ ├── LocksServlet.kt
│ │ ├── PointerManager.kt
│ │ ├── PointerServlet.kt
│ │ ├── ServerError.kt
│ │ ├── UnauthorizedError.kt
│ │ └── internal
│ │ ├── ObjectResponse.kt
│ │ └── ResponseWriter.kt
│ └── test
│ └── kotlin
│ └── ru
│ └── bozaro
│ └── gitlfs
│ └── server
│ ├── BatchTest.kt
│ ├── EmbeddedHttpServer.kt
│ ├── EmbeddedLfsServer.kt
│ ├── LocksTest.kt
│ ├── MemoryLockManager.kt
│ ├── MemoryStorage.kt
│ └── ServerTest.kt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.dat -text
2 | *.yml text eol=lf
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: "gradle"
6 | directory: "/"
7 | schedule:
8 | interval: "daily"
9 |
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | name: Test
6 | if: "!startsWith(github.ref, 'refs/tags/')"
7 | strategy:
8 | matrix:
9 | include:
10 | - java: 11
11 | - java: 17
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | - name: Set up Java
17 | uses: actions/setup-java@v4
18 | with:
19 | distribution: 'adopt'
20 | java-version: ${{ matrix.java }}
21 | - name: Run tests
22 | uses: eskatos/gradle-command-action@v3
23 | with:
24 | arguments: test
25 | publish:
26 | runs-on: ubuntu-latest
27 | needs: [ test ]
28 | # See https://github.com/actions/runner/issues/491#issuecomment-850884422 for explanation on why always() is needed
29 | if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') && github.repository == 'git-as-svn/git-lfs-java' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | - name: Set up Java
34 | uses: actions/setup-java@v4
35 | with:
36 | distribution: 'adopt'
37 | java-version: 11 # Build releases using oldest supported jdk
38 | - name: Publish to Sonatype
39 | uses: eskatos/gradle-command-action@v3
40 | env:
41 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
42 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
43 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
44 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
45 | with:
46 | arguments: publish closeAndReleaseStagingRepositories
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.local
3 |
4 | # Package Files #
5 | *.jar
6 | *.war
7 | *.ear
8 |
9 | # Idea
10 | *.iml
11 | .idea
12 |
13 | # Gradle
14 | .gradle
15 | .maven
16 | build
17 |
18 | # Virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
19 | hs_err_pid*
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | = git-lfs-java
2 | :project-handle: git-lfs-java
3 | :slug: bozaro/{project-handle}
4 | :uri-project: https://github.com/{slug}
5 | :uri-ci: {uri-project}/actions?query=branch%3Amaster
6 |
7 | image:{uri-project}/actions/workflows/ci.yml/badge.svg?branch=master[Build Status,link={uri-ci}]
8 | image:https://img.shields.io/maven-central/v/ru.bozaro.gitlfs/gitlfs-common.svg[Maven Central,link=http://mvnrepository.com/artifact/ru.bozaro.gitlfs]
9 |
10 | == What is this?
11 |
12 | This is Git LFS Java API implementation.
13 |
14 | This project contains:
15 |
16 | * gitlfs-common - Common structures for serialization/deserialization Git LFS messages
17 | * gitlfs-pointer - Git LFS pointer serialization/deserialization
18 | * gitlfs-client - API for uploading/downloading Git LFS objects from server
19 | * gitlfs-server - Servlets for creating custom LFS server
20 |
21 | == How to use?
22 |
23 | You can download the latest stable from http://mvnrepository.com/artifact/ru.bozaro.gitlfs[Maven Central].
24 |
25 | === Downloading object from Git LFS server
26 |
27 | [source,java]
28 | ----
29 | include::gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt[tags=download]
30 | ----
31 |
32 | === Uploading object to Git LFS server
33 |
34 | [source,java]
35 | ----
36 | include::gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt[tags=upload]
37 | ----
38 |
39 | === Embedded LFS server
40 |
41 | See link:gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/ServerTest.kt[] for example.
42 |
43 | == Changelog
44 |
45 | == 0.19.0
46 |
47 | * Downgrade SLF4J to 1.7.x
48 |
49 | === 0.18.0
50 |
51 | * Drop Java 8 support
52 | * Upgrade to Jakarta Servlet 5.0
53 | * Upgrade to Jetty 11
54 |
55 | === 0.17.0
56 |
57 | * Properly handle `Transfer-Encoding: chunked`.
58 | See https://github.com/bozaro/git-as-svn/issues/365[bozaro/git-as-svn#365]
59 |
60 | === 0.16.0
61 |
62 | * Update dependencies
63 |
64 | === 0.15.2
65 |
66 | * Send error message when lock create fails due to already existing lock
67 |
68 | === 0.15.1
69 |
70 | * Fix compatibility with GitLab LFS locks API
71 |
72 | === 0.15.0
73 |
74 | * Introduce Client.openObject methods
75 |
76 | === 0.14.1
77 |
78 | * Fix object verification to be compatible with git-lfs client
79 |
80 | === 0.14.0
81 |
82 | * Add LFS object verification server code
83 | * Fix broken handling of already existing LFS object
84 |
85 | === 0.13.3
86 |
87 | * Update dependencies
88 | * Fix deprecated Jackson API usage
89 |
90 | === 0.13.2
91 |
92 | * Fix ISO 8601 date formatting again
93 |
94 | === 0.13.1
95 |
96 | * LFS locking API fixes
97 | * Drop dependency on Guava
98 |
99 | === 0.13.0
100 |
101 | * https://github.com/git-lfs/git-lfs/blob/master/docs/api/locking.md[LFS locking] support
102 |
103 | === 0.12.1
104 |
105 | * Fix compatibility with Gitea LFS
106 |
107 | === 0.12.0
108 |
109 | * Update dependencies
110 | * Do not output \r in JSON on Windows
111 |
112 | === 0.11.1
113 |
114 | * Fix ISO 8601 date formatting
115 |
116 | === 0.11.0
117 |
118 | * JFrog Artifactory git-lfs compatibility (see #4)
119 |
120 | === 0.10.0
121 |
122 | * Add request/response content information to RequestException
123 |
124 | === 0.9.0
125 |
126 | * Update all dependencies
127 |
128 | === 0.8.0
129 |
130 | * Replace commons-httpclient:commons-httpclient:3.1 by org.apache.httpcomponents:httpclient:4.1.3
131 |
132 | === 0.7.0
133 |
134 | * Add header modification for replacing Basic authentication by Token
135 | * Don't ask password for SSH authentication
136 | * Add more informative HTTP error message
137 |
138 | === 0.6.0
139 |
140 | * Require JDK 8
141 | * High level batch API implementation
142 | * Add steam hash validation on download
143 |
144 | === 0.5.0
145 |
146 | * Server implemetation stabilization
147 | * Create multitheaded HttpClient by default
148 | * Fix single object downloading API
149 |
150 | === 0.4.0
151 |
152 | * Initial server implementation
153 | * Fix url concatenation
154 | * Fix downloading for not uploaded object (404 error)
155 | * Fix minor bugs
156 |
157 | === 0.3.0
158 |
159 | * Add authenticator for git-lfs-authenticate command (experimental)
160 | * Add AuthHelper class for simple AuthProvider creation
161 | * Fix verify url bug
162 | * Fix basic authentication
163 | * Fix already uploaded behaviour
164 |
165 | === 0.2.0
166 |
167 | * Support https://github.com/github/git-lfs/blob/master/docs/api/http-v1-batch.md[Git LFS v1 Batch API]
168 |
169 | === 0.1.0
170 |
171 | * Initial version;
172 | * Support https://github.com/github/git-lfs/blob/master/docs/api/http-v1-original.md[Git LFS v1 Original API]
173 | * Support https://github.com/github/git-lfs/blob/master/docs/spec.md[Git LFS pointer serialize/parse]
174 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.ajoberstar.grgit.Grgit
2 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat
3 | import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel
4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5 |
6 | val ossrhUsername: String? = System.getenv("OSSRH_USERNAME")
7 | val ossrhPassword: String? = System.getenv("OSSRH_PASSWORD")
8 | val signingKey: String? = System.getenv("SIGNING_KEY")
9 | val signingPassword: String? = System.getenv("SIGNING_PASSWORD")
10 |
11 | tasks.wrapper {
12 | gradleVersion = "8.5"
13 | distributionType = Wrapper.DistributionType.ALL
14 | }
15 |
16 | plugins {
17 | id("com.github.ben-manes.versions") version "0.52.0"
18 | id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
19 | id("org.ajoberstar.grgit") version "5.3.0"
20 | kotlin("jvm") version "2.1.21" apply false
21 | idea
22 | }
23 |
24 | val javaVersion = JavaVersion.VERSION_11
25 |
26 | allprojects {
27 | group = "ru.bozaro.gitlfs"
28 | version = "0.21.0-SNAPSHOT"
29 |
30 | apply(plugin = "idea")
31 | apply(plugin = "com.github.ben-manes.versions")
32 | apply(plugin = "org.jetbrains.kotlin.jvm")
33 |
34 | tasks.withType {
35 | kotlinOptions.jvmTarget = javaVersion.toString()
36 | }
37 |
38 | repositories {
39 | mavenCentral()
40 | }
41 | }
42 |
43 | idea {
44 | project.jdkName = javaVersion.name
45 | project.languageLevel = IdeaLanguageLevel(javaVersion)
46 | }
47 |
48 | subprojects {
49 | apply(plugin = "java-library")
50 | apply(plugin = "maven-publish")
51 | apply(plugin = "signing")
52 |
53 | configure {
54 | sourceCompatibility = javaVersion
55 | targetCompatibility = javaVersion
56 | }
57 |
58 | val api by configurations
59 | val testImplementation by configurations
60 |
61 | dependencies {
62 | api("com.google.code.findbugs:jsr305:3.0.2")
63 |
64 | testImplementation("com.google.guava:guava:33.4.8-jre")
65 | testImplementation("org.testng:testng:7.11.0")
66 | }
67 |
68 | idea {
69 | module {
70 | isDownloadJavadoc = true
71 | isDownloadSources = true
72 |
73 | // Workaround for https://youtrack.jetbrains.com/issue/IDEA-175172
74 | outputDir = file("build/classes/main")
75 | testOutputDir = file("build/classes/test")
76 | }
77 | }
78 |
79 | tasks.withType {
80 | options.encoding = "UTF-8"
81 | }
82 |
83 | tasks.withType {
84 | useTestNG {
85 | testLogging {
86 | exceptionFormat = TestExceptionFormat.FULL
87 | showStandardStreams = true
88 | }
89 | }
90 | }
91 |
92 | val javadoc by tasks.getting(Javadoc::class) {
93 | (options as? CoreJavadocOptions)?.addStringOption("Xdoclint:none", "-quiet")
94 | }
95 |
96 | val javadocJar by tasks.registering(Jar::class) {
97 | from(javadoc)
98 | archiveClassifier.set("javadoc")
99 | }
100 |
101 | val sourcesJar by tasks.registering(Jar::class) {
102 | val sourceSets: SourceSetContainer by project
103 | from(sourceSets["main"].allSource)
104 | archiveClassifier.set("sources")
105 | }
106 |
107 | configure {
108 | publications {
109 | create(project.name) {
110 | from(components["java"])
111 |
112 | artifact(sourcesJar.get())
113 | artifact(javadocJar.get())
114 |
115 | pom {
116 | name.set(project.name)
117 |
118 | val pomDescription = description
119 | afterEvaluate {
120 | pomDescription.set(project.description)
121 | }
122 |
123 | url.set("https://github.com/bozaro/git-lfs-java")
124 |
125 | scm {
126 | connection.set("scm:git:git://github.com/bozaro/git-lfs-java.git")
127 | tag.set(Grgit.open(mapOf("dir" to rootDir)).head().id)
128 | url.set("https://github.com/bozaro/git-lfs-java")
129 | }
130 |
131 | licenses {
132 | license {
133 | name.set("Lesser General Public License, version 3 or greater")
134 | url.set("https://www.gnu.org/licenses/lgpl-3.0.html")
135 | }
136 | }
137 |
138 | developers {
139 | developer {
140 | id.set("bozaro")
141 | name.set("Artem V. Navrotskiy")
142 | email.set("bozaro@yandex.ru")
143 | }
144 |
145 | developer {
146 | id.set("slonopotamus")
147 | name.set("Marat Radchenko")
148 | email.set("marat@slonopotamus.org")
149 | }
150 | }
151 | }
152 | }
153 | }
154 | }
155 |
156 | configure {
157 | isRequired = signingKey != ""
158 |
159 | useInMemoryPgpKeys(signingKey, signingPassword)
160 |
161 | val publishing: PublishingExtension by project.extensions
162 | sign(publishing.publications)
163 | }
164 | }
165 |
166 | nexusPublishing {
167 | repositories {
168 | sonatype {
169 | packageGroup.set("ru.bozaro")
170 | stagingProfileId.set("365bc6dc8b7aa3")
171 | username.set(ossrhUsername)
172 | password.set(ossrhPassword)
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/gitlfs-client/build.gradle.kts:
--------------------------------------------------------------------------------
1 | description = "Java Git-LFS client library"
2 |
3 | dependencies {
4 | api(project(":gitlfs-common"))
5 | api("org.apache.httpcomponents:httpclient:4.5.14")
6 | implementation("org.slf4j:slf4j-api:1.7.36")
7 |
8 | testImplementation("org.yaml:snakeyaml:1.33")
9 | testRuntimeOnly("org.slf4j:slf4j-simple:1.7.36")
10 | }
11 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/AuthHelper.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import ru.bozaro.gitlfs.client.auth.AuthProvider
4 | import ru.bozaro.gitlfs.client.auth.BasicAuthProvider
5 | import ru.bozaro.gitlfs.client.auth.ExternalAuthProvider
6 | import java.net.MalformedURLException
7 | import java.net.URI
8 | import java.net.URISyntaxException
9 |
10 | /**
11 | * Utility class.
12 | *
13 | * @author Artem V. Navrotskiy
14 | */
15 | object AuthHelper {
16 | /**
17 | * Create AuthProvider by gitURL.
18 | *
19 | *
20 | * Supported URL formats:
21 | *
22 | *
23 | * * https://user:passw0rd@github.com/foo/bar.git
24 | * * http://user:passw0rd@github.com/foo/bar.git
25 | * * git://user:passw0rd@github.com/foo/bar.git
26 | * * ssh://git@github.com/foo/bar.git
27 | * * git@github.com:foo/bar.git
28 | *
29 | *
30 | * Detail Git URL format: https://git-scm.com/book/ch4-1.html
31 | *
32 | * @param gitURL URL to repository.
33 | * @return Created auth provider.
34 | */
35 | @kotlin.jvm.JvmStatic
36 | @Throws(MalformedURLException::class)
37 | fun create(gitURL: String): AuthProvider {
38 | if (gitURL.contains("://")) {
39 | val uri = URI.create(gitURL)
40 | val path = uri.path
41 | return when (uri.scheme) {
42 | "https", "http", "git" -> BasicAuthProvider(join(uri, "info/lfs"))
43 | "ssh" -> ExternalAuthProvider(uri.authority, if (path.startsWith("/")) path.substring(1) else path)
44 | else -> throw MalformedURLException("Can't find authenticator for scheme: " + uri.scheme)
45 | }
46 | }
47 | return ExternalAuthProvider(gitURL)
48 | }
49 |
50 | fun join(href: URI, vararg path: String): URI {
51 | return try {
52 | var uri = URI(href.scheme, href.authority, href.path + if (href.path.endsWith("/")) "" else "/", null, null)
53 | for (fragment in path) uri = uri.resolve(fragment)
54 | uri
55 | } catch (e: URISyntaxException) {
56 | throw IllegalStateException(e)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchDownloader.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import ru.bozaro.gitlfs.client.internal.BatchWorker
4 | import ru.bozaro.gitlfs.client.internal.Work
5 | import ru.bozaro.gitlfs.client.io.StreamHandler
6 | import ru.bozaro.gitlfs.common.data.BatchItem
7 | import ru.bozaro.gitlfs.common.data.LinkType
8 | import ru.bozaro.gitlfs.common.data.Meta
9 | import ru.bozaro.gitlfs.common.data.Operation
10 | import java.io.IOException
11 | import java.util.concurrent.CompletableFuture
12 | import java.util.concurrent.ExecutorService
13 |
14 | /**
15 | * Batching downloader client.
16 | *
17 | * @author Artem V. Navrotskiy
18 | */
19 | class BatchDownloader constructor(client: Client, pool: ExecutorService, settings: BatchSettings = BatchSettings()) :
20 | BatchWorker, Any?>(client, pool, settings, Operation.Download) {
21 | fun download(meta: Meta, callback: StreamHandler): CompletableFuture {
22 | return enqueue(meta, callback) as CompletableFuture
23 | }
24 |
25 | override fun objectTask(state: State, Any?>, item: BatchItem): Work? {
26 | // Invalid links data
27 | if (!item.links.containsKey(LinkType.Download)) {
28 | state.future.completeExceptionally(IOException("Download link not found"))
29 | return null
30 | }
31 | // Already processed
32 | return Work { client.getObject(item, item, state.context) }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchSettings.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import ru.bozaro.gitlfs.common.Constants.BATCH_SIZE
4 | import kotlin.math.max
5 | import kotlin.math.min
6 |
7 | /**
8 | * Batch settings.
9 | *
10 | * @author Artem V. Navrotskiy @users.noreply.github.com>
11 | */
12 | class BatchSettings {
13 | /**
14 | * Maximum objects in batch request.
15 | */
16 | var limit: Int = BATCH_SIZE
17 | private set
18 |
19 | /**
20 | * Minimum download/upload requests in queue for next batch request.
21 | */
22 | var threshold = 10
23 | private set
24 |
25 | /**
26 | * Retry on failure count.
27 | */
28 | var retryCount = 3
29 | private set
30 |
31 | constructor()
32 | constructor(limit: Int, threshold: Int, retryCount: Int) {
33 | this.limit = limit
34 | this.threshold = threshold
35 | this.retryCount = retryCount
36 | }
37 |
38 | fun setLimit(limit: Int): BatchSettings {
39 | this.limit = min(limit, 1)
40 | return this
41 | }
42 |
43 | fun setThreshold(threshold: Int): BatchSettings {
44 | this.threshold = max(threshold, 0)
45 | return this
46 | }
47 |
48 | fun setRetryCount(retryCount: Int): BatchSettings {
49 | this.retryCount = max(retryCount, 1)
50 | return this
51 | }
52 |
53 | override fun toString(): String {
54 | return "BatchSettings{" +
55 | "limit=" + limit +
56 | ", threshold=" + threshold +
57 | ", retryCount=" + retryCount +
58 | '}'
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchUploader.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import ru.bozaro.gitlfs.client.internal.BatchWorker
4 | import ru.bozaro.gitlfs.client.internal.Work
5 | import ru.bozaro.gitlfs.client.io.StreamProvider
6 | import ru.bozaro.gitlfs.common.data.BatchItem
7 | import ru.bozaro.gitlfs.common.data.LinkType
8 | import ru.bozaro.gitlfs.common.data.Meta
9 | import ru.bozaro.gitlfs.common.data.Operation
10 | import java.util.concurrent.CompletableFuture
11 | import java.util.concurrent.ExecutorService
12 |
13 | /**
14 | * Batching uploader client.
15 | *
16 | * @author Artem V. Navrotskiy
17 | */
18 | class BatchUploader constructor(client: Client, pool: ExecutorService, settings: BatchSettings = BatchSettings()) :
19 | BatchWorker(client, pool, settings, Operation.Upload) {
20 | /**
21 | * This method computes stream metadata and upload object.
22 | *
23 | * @param streamProvider Stream provider.
24 | * @return Return future with upload result.
25 | */
26 | fun upload(streamProvider: StreamProvider): CompletableFuture {
27 | val future = CompletableFuture()
28 | pool.submit {
29 | try {
30 | future.complete(Client.generateMeta(streamProvider))
31 | } catch (e: Throwable) {
32 | future.completeExceptionally(e)
33 | }
34 | }
35 | return future.thenCompose { meta: Meta -> upload(meta, streamProvider) }
36 | }
37 |
38 | /**
39 | * This method start uploading object to server.
40 | *
41 | * @param meta Object metadata.
42 | * @param streamProvider Stream provider.
43 | * @return Return future with upload result. For same objects can return same future.
44 | */
45 | fun upload(meta: Meta, streamProvider: StreamProvider): CompletableFuture {
46 | return enqueue(meta, streamProvider)
47 | }
48 |
49 | override fun objectTask(state: State, item: BatchItem): Work? {
50 | return if (item.links.containsKey(LinkType.Upload)) {
51 | // Wait for upload.
52 | Work {
53 | client.putObject(state.context, state.meta, item)
54 | null
55 | }
56 | } else {
57 | // Already uploaded.
58 | state.future.complete(state.meta)
59 | null
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/HttpExecutor.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.apache.http.client.methods.CloseableHttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 | import java.io.Closeable
6 | import java.io.IOException
7 |
8 | /**
9 | * Abstract class for HTTP connection execution.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | interface HttpExecutor : Closeable {
14 | @Throws(IOException::class)
15 | fun executeMethod(request: HttpUriRequest): CloseableHttpResponse
16 | }
17 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/AuthProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.auth
2 |
3 | import ru.bozaro.gitlfs.common.data.Link
4 | import ru.bozaro.gitlfs.common.data.Operation
5 | import java.io.IOException
6 |
7 | /**
8 | * Authentication provider.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | interface AuthProvider {
13 | /**
14 | * Get auth ru.bozaro.gitlfs.common.data.
15 | * Auth ru.bozaro.gitlfs.common.data can be cached in this method.
16 | *
17 | * @param operation Operation type.
18 | * @return ru.bozaro.gitlfs.common.data.
19 | */
20 | @Throws(IOException::class)
21 | fun getAuth(operation: Operation): Link
22 |
23 | /**
24 | * Set auth as expired.
25 | *
26 | * @param operation Operation type.
27 | * @param auth Expired auth ru.bozaro.gitlfs.common.data.
28 | */
29 | fun invalidateAuth(operation: Operation, auth: Link)
30 | }
31 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/BasicAuthProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.auth
2 |
3 | import org.apache.commons.codec.binary.Base64
4 | import ru.bozaro.gitlfs.common.Constants.HEADER_AUTHORIZATION
5 | import ru.bozaro.gitlfs.common.data.Link
6 | import ru.bozaro.gitlfs.common.data.Operation
7 | import java.net.URI
8 | import java.net.URISyntaxException
9 | import java.nio.charset.StandardCharsets
10 | import java.util.*
11 |
12 | /**
13 | * Auth provider for basic authentication.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class BasicAuthProvider constructor(href: URI, login: String? = null, password: String? = null) : AuthProvider {
18 | private var auth: Link
19 |
20 | override fun getAuth(operation: Operation): Link {
21 | return auth
22 | }
23 |
24 | override fun invalidateAuth(operation: Operation, auth: Link) {}
25 |
26 | companion object {
27 | private fun isEmpty(value: String?): Boolean {
28 | return value == null || value.isEmpty()
29 | }
30 |
31 | private fun extractLogin(userInfo: String?): String {
32 | if (userInfo == null) return ""
33 | val separator = userInfo.indexOf(':')
34 | return if (separator >= 0) userInfo.substring(0, separator) else userInfo
35 | }
36 |
37 | private fun extractPassword(userInfo: String?): String {
38 | if (userInfo == null) return ""
39 | val separator = userInfo.indexOf(':')
40 | return if (separator >= 0) userInfo.substring(separator + 1) else ""
41 | }
42 | }
43 |
44 | init {
45 | val authLogin: String? = if (isEmpty(login)) {
46 | extractLogin(href.userInfo)
47 | } else {
48 | login
49 | }
50 | val authPassword: String? = if (isEmpty(password)) {
51 | extractPassword(href.userInfo)
52 | } else {
53 | password
54 | }
55 | val header = TreeMap()
56 | val userInfo = "$authLogin:$authPassword"
57 | header[HEADER_AUTHORIZATION] =
58 | "Basic " + String(Base64.encodeBase64(userInfo.toByteArray(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)
59 | try {
60 | val scheme = if ("git" == href.scheme) "https" else href.scheme
61 | auth = Link(URI(scheme, href.authority, href.path, null, null), Collections.unmodifiableMap(header), null)
62 | } catch (e: URISyntaxException) {
63 | throw IllegalStateException(e)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/CachedAuthProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.auth
2 |
3 | import ru.bozaro.gitlfs.common.data.Link
4 | import ru.bozaro.gitlfs.common.data.Operation
5 | import java.io.IOException
6 | import java.util.*
7 | import java.util.concurrent.ConcurrentHashMap
8 | import java.util.concurrent.ConcurrentMap
9 |
10 | /**
11 | * Get authentication ru.bozaro.gitlfs.common.data from external application.
12 | * This AuthProvider is EXPERIMENTAL and it can only be used at your own risk.
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | abstract class CachedAuthProvider : AuthProvider {
17 | private val authCache: ConcurrentMap
18 |
19 | private val locks: EnumMap = createLocks()
20 |
21 | @Throws(IOException::class)
22 | override fun getAuth(operation: Operation): Link {
23 | var auth = authCache[operation]
24 | if (auth == null) {
25 | synchronized(locks[operation]!!) {
26 | auth = authCache[operation]
27 | if (auth == null) {
28 | try {
29 | auth = getAuthUncached(operation)
30 | authCache[operation] = auth
31 | } catch (e: InterruptedException) {
32 | throw IOException(e)
33 | }
34 | }
35 | }
36 | }
37 | return auth!!
38 | }
39 |
40 | @Throws(IOException::class, InterruptedException::class)
41 | protected abstract fun getAuthUncached(operation: Operation): Link
42 |
43 | override fun invalidateAuth(operation: Operation, auth: Link) {
44 | authCache.remove(operation, auth)
45 | }
46 |
47 | companion object {
48 | private fun createLocks(): EnumMap {
49 | val result = EnumMap(Operation::class.java)
50 | for (value in Operation.values()) {
51 | result[value] = Any()
52 | }
53 | return result
54 | }
55 | }
56 |
57 | init {
58 | authCache = ConcurrentHashMap(Operation.values().size)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/ExternalAuthProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.auth
2 |
3 | import ru.bozaro.gitlfs.common.JsonHelper
4 | import ru.bozaro.gitlfs.common.data.Link
5 | import ru.bozaro.gitlfs.common.data.Operation
6 | import java.io.ByteArrayOutputStream
7 | import java.io.IOException
8 | import java.net.MalformedURLException
9 |
10 | /**
11 | * Get authentication ru.bozaro.gitlfs.common.data from external application.
12 | * This AuthProvider is EXPERIMENTAL and it can only be used at your own risk.
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | open class ExternalAuthProvider : CachedAuthProvider {
17 | private val authority: String
18 | private val path: String
19 |
20 | /**
21 | * Create authentication wrapper for git-lfs-authenticate command.
22 | *
23 | * @param gitUrl Git URL like: git@github.com:bozaro/git-lfs-java.git
24 | */
25 | constructor(gitUrl: String) {
26 | val separator = gitUrl.indexOf(':')
27 | if (separator < 0) {
28 | throw MalformedURLException("Can't find separator ':' in gitUrl: $gitUrl")
29 | }
30 | authority = gitUrl.substring(0, separator)
31 | path = gitUrl.substring(separator + 1)
32 | }
33 |
34 | /**
35 | * Create authentication wrapper for git-lfs-authenticate command.
36 | *
37 | * @param authority SSH server authority with user name
38 | * @param path Repostiry path
39 | */
40 | constructor(authority: String, path: String) {
41 | this.authority = authority
42 | this.path = path
43 | }
44 |
45 | @Throws(IOException::class, InterruptedException::class)
46 | override fun getAuthUncached(operation: Operation): Link {
47 | val builder = ProcessBuilder()
48 | .command(*getCommand(operation))
49 | .redirectError(ProcessBuilder.Redirect.PIPE)
50 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
51 | val process = builder.start()
52 | process.outputStream.close()
53 | val stdoutStream = process.inputStream
54 | val stdoutData = ByteArrayOutputStream()
55 | val buffer = ByteArray(0x10000)
56 | while (true) {
57 | val read = stdoutStream.read(buffer)
58 | if (read <= 0) break
59 | stdoutData.write(buffer, 0, read)
60 | }
61 | val exitValue = process.waitFor()
62 | if (exitValue != 0) {
63 | throw IOException("Command returned with non-zero exit code " + exitValue + ": " + builder.command().toTypedArray().contentToString())
64 | }
65 | return JsonHelper.mapper.readValue(stdoutData.toByteArray(), Link::class.java)
66 | }
67 |
68 | private fun getCommand(operation: Operation): Array {
69 | return arrayOf(
70 | "ssh",
71 | authority,
72 | "-oBatchMode=yes",
73 | "-C",
74 | "git-lfs-authenticate",
75 | path,
76 | operation.toValue()
77 | )
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/ForbiddenException.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.exceptions
2 |
3 | import org.apache.http.HttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 |
6 | /**
7 | * Forbidden HTTP exception.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | class ForbiddenException(request: HttpUriRequest, response: HttpResponse) : RequestException(request, response)
12 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/RequestException.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.exceptions
2 |
3 | import org.apache.http.HttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 | import java.io.IOException
6 |
7 | /**
8 | * Simple HTTP exception.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | open class RequestException(private val request: HttpUriRequest, private val response: HttpResponse) : IOException() {
13 | val statusCode: Int
14 | get() = response.statusLine.statusCode
15 | override val message: String
16 | get() {
17 | val statusLine = response.statusLine
18 | return request.uri.toString() + " - " + statusLine.statusCode + " (" + statusLine.reasonPhrase + ")"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/UnauthorizedException.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.exceptions
2 |
3 | import org.apache.http.HttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 |
6 | /**
7 | * Unauthorized HTTP exception.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | class UnauthorizedException(request: HttpUriRequest, response: HttpResponse) : RequestException(request, response)
12 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/HttpClientExecutor.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import org.apache.http.client.methods.CloseableHttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 | import org.apache.http.impl.client.CloseableHttpClient
6 | import ru.bozaro.gitlfs.client.HttpExecutor
7 | import java.io.IOException
8 |
9 | /**
10 | * Simple HttpClient wrapper.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class HttpClientExecutor(private val http: CloseableHttpClient) : HttpExecutor {
15 | @Throws(IOException::class)
16 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse {
17 | return http.execute(request)
18 | }
19 |
20 | @Throws(IOException::class)
21 | override fun close() {
22 | http.close()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/JsonPost.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import org.apache.http.HttpResponse
6 | import org.apache.http.client.methods.HttpPost
7 | import org.apache.http.entity.ByteArrayEntity
8 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
9 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
10 | import java.io.IOException
11 |
12 | /**
13 | * POST simple JSON request.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class JsonPost(private val req: Req, private val type: Class) : Request {
18 | @Throws(JsonProcessingException::class)
19 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
20 | val method = HttpPost(url)
21 | method.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
22 | val entity = ByteArrayEntity(mapper.writeValueAsBytes(req))
23 | entity.setContentType(MIME_LFS_JSON)
24 | method.entity = entity
25 | return LfsRequest(method, entity)
26 | }
27 |
28 | @Throws(IOException::class)
29 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Res {
30 | return mapper.readValue(response.entity.content, type)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LfsRequest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import org.apache.http.client.methods.HttpUriRequest
4 | import org.apache.http.entity.AbstractHttpEntity
5 | import org.apache.http.protocol.HTTP
6 |
7 | class LfsRequest internal constructor(private val request: HttpUriRequest, private val entity: AbstractHttpEntity?) {
8 | fun addHeaders(headers: Map): HttpUriRequest {
9 | for ((key, value) in headers) {
10 | if (HTTP.TRANSFER_ENCODING == key) {
11 | /*
12 | See https://github.com/bozaro/git-as-svn/issues/365
13 | LFS-server can ask us to respond with chunked body via setting "Transfer-Encoding: chunked" HTTP header in LFS link
14 | Unfortunately, we cannot pass it as-is to response HTTP headers, see RequestContent#process.
15 | If it sees that Transfer-Encoding header was set, it throws exception immediately.
16 | So instead, we suppress addition of Transfer-Encoding header and set entity to be chunked here.
17 | RequestContent#process will see that HttpEntity#isChunked returns true and will set correct Transfer-Encoding header.
18 | */
19 | if (entity != null) {
20 | val chunked = HTTP.CHUNK_CODING == value
21 | entity.isChunked = chunked
22 | }
23 | } else {
24 | request.addHeader(key, value)
25 | }
26 | }
27 | return request
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LockCreate.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import org.apache.http.HttpResponse
6 | import org.apache.http.HttpStatus
7 | import org.apache.http.client.methods.HttpPost
8 | import org.apache.http.entity.AbstractHttpEntity
9 | import org.apache.http.entity.ByteArrayEntity
10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
12 | import ru.bozaro.gitlfs.common.data.*
13 | import java.io.IOException
14 |
15 | class LockCreate(private val path: String, private val ref: Ref?) : Request {
16 | @Throws(JsonProcessingException::class)
17 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
18 | val req = HttpPost(url)
19 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
20 | val createLockReq = CreateLockReq(path, ref)
21 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(createLockReq))
22 | entity.setContentType(MIME_LFS_JSON)
23 | req.entity = entity
24 | return LfsRequest(req, entity)
25 | }
26 |
27 | @Throws(IOException::class)
28 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Res {
29 | return when (response.statusLine.statusCode) {
30 | HttpStatus.SC_CREATED -> Res(true, mapper.readValue(response.entity.content, CreateLockRes::class.java).lock, null)
31 | HttpStatus.SC_CONFLICT -> {
32 | val res = mapper.readValue(response.entity.content, LockConflictRes::class.java)
33 | Res(false, res.lock, res.message)
34 | }
35 | else -> throw IllegalStateException()
36 | }
37 | }
38 |
39 | override fun statusCodes(): IntArray {
40 | return intArrayOf(
41 | HttpStatus.SC_CREATED,
42 | HttpStatus.SC_CONFLICT)
43 | }
44 |
45 | class Res(val isSuccess: Boolean, val lock: Lock, val message: String?)
46 | }
47 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LockDelete.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import org.apache.http.HttpResponse
6 | import org.apache.http.HttpStatus
7 | import org.apache.http.client.methods.HttpPost
8 | import org.apache.http.entity.AbstractHttpEntity
9 | import org.apache.http.entity.ByteArrayEntity
10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
12 | import ru.bozaro.gitlfs.common.data.DeleteLockReq
13 | import ru.bozaro.gitlfs.common.data.DeleteLockRes
14 | import ru.bozaro.gitlfs.common.data.Lock
15 | import ru.bozaro.gitlfs.common.data.Ref
16 | import java.io.IOException
17 |
18 | class LockDelete(private val force: Boolean, private val ref: Ref?) : Request {
19 | @Throws(JsonProcessingException::class)
20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
21 | val req = HttpPost(url)
22 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
23 | val createLockReq = DeleteLockReq(force, ref)
24 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(createLockReq))
25 | entity.setContentType(MIME_LFS_JSON)
26 | req.entity = entity
27 | return LfsRequest(req, entity)
28 | }
29 |
30 | @Throws(IOException::class)
31 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Lock? {
32 | return when (response.statusLine.statusCode) {
33 | HttpStatus.SC_OK -> mapper.readValue(response.entity.content, DeleteLockRes::class.java).lock
34 | HttpStatus.SC_NOT_FOUND -> null
35 | else -> throw IllegalStateException()
36 | }
37 | }
38 |
39 | override fun statusCodes(): IntArray {
40 | return intArrayOf(
41 | HttpStatus.SC_OK,
42 | HttpStatus.SC_NOT_FOUND
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LocksList.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.client.methods.HttpGet
6 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
7 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
8 | import ru.bozaro.gitlfs.common.data.LocksRes
9 | import java.io.IOException
10 |
11 | class LocksList : Request {
12 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
13 | val req = HttpGet(url)
14 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
15 | return LfsRequest(req, null)
16 | }
17 |
18 | @Throws(IOException::class)
19 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): LocksRes {
20 | return mapper.readValue(response.entity.content, LocksRes::class.java)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/MetaGet.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.HttpStatus
6 | import org.apache.http.client.methods.HttpGet
7 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
8 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
9 | import ru.bozaro.gitlfs.common.data.ObjectRes
10 | import java.io.IOException
11 |
12 | /**
13 | * GET object metadata request.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class MetaGet : Request {
18 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
19 | val req = HttpGet(url)
20 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
21 | return LfsRequest(req, null)
22 | }
23 |
24 | @Throws(IOException::class)
25 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): ObjectRes? {
26 | return when (response.statusLine.statusCode) {
27 | HttpStatus.SC_OK -> mapper.readValue(response.entity.content, ObjectRes::class.java)
28 | HttpStatus.SC_NOT_FOUND -> null
29 | else -> throw IllegalStateException()
30 | }
31 | }
32 |
33 | override fun statusCodes(): IntArray {
34 | return intArrayOf(
35 | HttpStatus.SC_OK,
36 | HttpStatus.SC_NOT_FOUND
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/MetaPost.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import org.apache.http.HttpResponse
6 | import org.apache.http.HttpStatus
7 | import org.apache.http.client.methods.HttpPost
8 | import org.apache.http.entity.AbstractHttpEntity
9 | import org.apache.http.entity.ByteArrayEntity
10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
12 | import ru.bozaro.gitlfs.common.data.Meta
13 | import ru.bozaro.gitlfs.common.data.ObjectRes
14 | import java.io.IOException
15 |
16 | /**
17 | * POST object metadata request.
18 | *
19 | * @author Artem V. Navrotskiy
20 | */
21 | class MetaPost(private val meta: Meta) : Request {
22 | @Throws(JsonProcessingException::class)
23 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
24 | val req = HttpPost(url)
25 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
26 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(meta))
27 | entity.setContentType(MIME_LFS_JSON)
28 | req.entity = entity
29 | return LfsRequest(req, entity)
30 | }
31 |
32 | @Throws(IOException::class)
33 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): ObjectRes? {
34 | return when (response.statusLine.statusCode) {
35 | HttpStatus.SC_OK -> null
36 | HttpStatus.SC_ACCEPTED -> mapper.readValue(response.entity.content, ObjectRes::class.java)
37 | else -> throw IllegalStateException()
38 | }
39 | }
40 |
41 | override fun statusCodes(): IntArray {
42 | return intArrayOf(
43 | HttpStatus.SC_OK,
44 | HttpStatus.SC_ACCEPTED
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectGet.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.client.methods.HttpGet
6 | import ru.bozaro.gitlfs.client.io.StreamHandler
7 | import java.io.IOException
8 |
9 | /**
10 | * GET object request.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class ObjectGet(private val handler: StreamHandler) : Request {
15 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
16 | val req = HttpGet(url)
17 | return LfsRequest(req, null)
18 | }
19 |
20 | @Throws(IOException::class)
21 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): T {
22 | return handler.accept(response.entity.content)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectPut.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.HttpStatus
6 | import org.apache.http.client.methods.HttpPut
7 | import org.apache.http.entity.AbstractHttpEntity
8 | import org.apache.http.entity.InputStreamEntity
9 | import ru.bozaro.gitlfs.client.io.StreamProvider
10 | import ru.bozaro.gitlfs.common.Constants.MIME_BINARY
11 | import java.io.IOException
12 |
13 | /**
14 | * PUT object request.
15 | *
16 | * @author Artem V. Navrotskiy
17 | */
18 | class ObjectPut(private val streamProvider: StreamProvider, private val size: Long) : Request {
19 | @Throws(IOException::class)
20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
21 | val req = HttpPut(url)
22 | val entity: AbstractHttpEntity = InputStreamEntity(streamProvider.stream, size)
23 | entity.setContentType(MIME_BINARY)
24 | req.entity = entity
25 | return LfsRequest(req, entity)
26 | }
27 |
28 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Void? {
29 | return null
30 | }
31 |
32 | override fun statusCodes(): IntArray {
33 | return intArrayOf(
34 | HttpStatus.SC_OK,
35 | HttpStatus.SC_CREATED
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectVerify.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.client.methods.HttpPost
6 | import org.apache.http.entity.AbstractHttpEntity
7 | import org.apache.http.entity.ByteArrayEntity
8 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT
9 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
10 | import ru.bozaro.gitlfs.common.data.Meta
11 | import java.io.IOException
12 |
13 | /**
14 | * Object verification after upload request.
15 | *
16 | * @author Artem V. Navrotskiy
17 | */
18 | class ObjectVerify(private val meta: Meta) : Request {
19 | @Throws(IOException::class)
20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest {
21 | val req = HttpPost(url)
22 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON)
23 | val content = mapper.writeValueAsBytes(meta)
24 | val entity: AbstractHttpEntity = ByteArrayEntity(content)
25 | entity.setContentType(MIME_LFS_JSON)
26 | req.entity = entity
27 | return LfsRequest(req, entity)
28 | }
29 |
30 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Void? {
31 | return null
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/Request.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.apache.http.HttpResponse
5 | import org.apache.http.HttpStatus
6 | import java.io.IOException
7 |
8 | /**
9 | * Single HTTP request.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | interface Request {
14 | @Throws(IOException::class)
15 | fun createRequest(mapper: ObjectMapper, url: String): LfsRequest
16 |
17 | @Throws(IOException::class)
18 | fun processResponse(mapper: ObjectMapper, response: HttpResponse): R
19 |
20 | /**
21 | * Success status codes.
22 | *
23 | * @return Success status codes.
24 | */
25 | fun statusCodes(): IntArray {
26 | return intArrayOf(HttpStatus.SC_OK)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/Work.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.internal
2 |
3 | import ru.bozaro.gitlfs.common.data.Link
4 | import java.io.IOException
5 |
6 | /**
7 | * Work with same auth ru.bozaro.gitlfs.common.data.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | fun interface Work {
12 | @Throws(IOException::class)
13 | fun exec(auth: Link): R
14 | }
15 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/ByteArrayStreamProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.io.ByteArrayInputStream
4 | import java.io.InputStream
5 |
6 | /**
7 | * Create stream by bytes.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | open class ByteArrayStreamProvider(private val data: ByteArray) : StreamProvider {
12 | override val stream: InputStream
13 | get() = ByteArrayInputStream(data)
14 | }
15 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/FileStreamProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.io.File
4 | import java.io.FileInputStream
5 | import java.io.IOException
6 | import java.io.InputStream
7 |
8 | /**
9 | * Create stream from file.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | class FileStreamProvider(private val file: File) : StreamProvider {
14 | @get:Throws(IOException::class)
15 | override val stream: InputStream
16 | get() = FileInputStream(file)
17 | }
18 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StreamHandler.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.io.IOException
4 | import java.io.InputStream
5 |
6 | /**
7 | * Interface for handle stream of downloading ru.bozaro.gitlfs.common.data.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | fun interface StreamHandler {
12 | @Throws(IOException::class)
13 | fun accept(inputStream: InputStream): T
14 | }
15 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StreamProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.io.IOException
4 | import java.io.InputStream
5 |
6 | /**
7 | * Stream provider.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | interface StreamProvider {
12 | @get:Throws(IOException::class)
13 | val stream: InputStream
14 | }
15 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StringStreamProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.nio.charset.StandardCharsets
4 |
5 | /**
6 | * Create stream from string.
7 | *
8 | * @author Artem V. Navrotskiy
9 | */
10 | class StringStreamProvider(data: String) : ByteArrayStreamProvider(data.toByteArray(StandardCharsets.UTF_8))
11 |
--------------------------------------------------------------------------------
/gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/UrlStreamProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client.io
2 |
3 | import java.io.IOException
4 | import java.io.InputStream
5 | import java.net.URL
6 |
7 | /**
8 | * Create stream by URL.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | class UrlStreamProvider(private val url: URL) : StreamProvider {
13 | @get:Throws(IOException::class)
14 | override val stream: InputStream
15 | get() = url.openStream()
16 | }
17 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/AuthHelperTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.DataProvider
5 | import org.testng.annotations.Test
6 | import ru.bozaro.gitlfs.client.AuthHelper.join
7 | import java.net.URI
8 |
9 | /**
10 | * Tests for AuthHelper.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class AuthHelperTest {
15 | @Test(dataProvider = "joinUrlProvider")
16 | fun joinUrl(base: String, str: String, expected: String) {
17 | Assert.assertEquals(join(URI.create(base), str), URI.create(expected))
18 | }
19 |
20 | @DataProvider(name = "joinUrlProvider")
21 | fun joinUrlProvider(): Array> {
22 | return arrayOf(
23 | arrayOf("http://test.ru/foo", "bar", "http://test.ru/foo/bar"),
24 | arrayOf("http://test.ru/foo/", "bar", "http://test.ru/foo/bar"),
25 | arrayOf("http://test.ru/foo", "/bar", "http://test.ru/bar"),
26 | arrayOf("http://test.ru/foo/", "/bar", "http://test.ru/bar"),
27 | arrayOf("https://test.ru/foo/", "http://foo.ru/bar", "http://foo.ru/bar")
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/ClientLegacyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import com.google.common.io.ByteStreams
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import ru.bozaro.gitlfs.client.exceptions.ForbiddenException
7 | import ru.bozaro.gitlfs.client.io.StringStreamProvider
8 | import java.io.FileNotFoundException
9 | import java.io.IOException
10 | import java.io.InputStream
11 | import java.nio.charset.StandardCharsets
12 |
13 | /**
14 | * Replay tests for https://github.com/github/git-lfs/blob/master/docs/api/http-v1-original.md
15 | *
16 | * @author Artem V. Navrotskiy
17 | */
18 | class ClientLegacyTest {
19 | /**
20 | * Simple upload.
21 | */
22 | @Test
23 | @Throws(IOException::class)
24 | fun legacyUpload01() {
25 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-01.yml")
26 | val client = Client(FakeAuthProvider(false), replay)
27 | Assert.assertTrue(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015")))
28 | replay.close()
29 | }
30 |
31 | /**
32 | * Forbidden.
33 | */
34 | @Test(expectedExceptions = [ForbiddenException::class])
35 | @Throws(IOException::class)
36 | fun legacyUpload02() {
37 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-02.yml")
38 | val client = Client(FakeAuthProvider(false), replay)
39 | Assert.assertFalse(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015")))
40 | replay.close()
41 | }
42 |
43 | /**
44 | * Expired token.
45 | */
46 | @Test
47 | @Throws(IOException::class)
48 | fun legacyUpload03() {
49 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-03.yml")
50 | val client = Client(FakeAuthProvider(false), replay)
51 | Assert.assertTrue(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015")))
52 | replay.close()
53 | }
54 |
55 | /**
56 | * Already uploaded,
57 | */
58 | @Test
59 | @Throws(IOException::class)
60 | fun legacyUpload04() {
61 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-04.yml")
62 | val client = Client(FakeAuthProvider(false), replay)
63 | Assert.assertFalse(client.putObject(StringStreamProvider("Hello, world!!!")))
64 | replay.close()
65 | }
66 |
67 | /**
68 | * Simple download
69 | */
70 | @Test
71 | @Throws(IOException::class)
72 | fun legacyDownload01() {
73 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-download-01.yml")
74 | val client = Client(FakeAuthProvider(false), replay)
75 | val data =
76 | client.getObject("b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e") { `in`: InputStream ->
77 | ByteStreams.toByteArray(`in`)
78 | }
79 | Assert.assertEquals(String(data, StandardCharsets.UTF_8), "Fri Oct 02 21:07:33 MSK 2015")
80 | replay.close()
81 | }
82 |
83 | /**
84 | * Download not uploaded object
85 | */
86 | @Test
87 | @Throws(IOException::class)
88 | fun legacyDownload02() {
89 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-download-02.yml")
90 | val client = Client(FakeAuthProvider(false), replay)
91 | try {
92 | client.getObject("01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b") { `in`: InputStream ->
93 | ByteStreams.toByteArray(`in`)
94 | }
95 | Assert.fail()
96 | } catch (ignored: FileNotFoundException) {
97 | }
98 | replay.close()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/ClientLocksTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import ru.bozaro.gitlfs.common.LockConflictException
6 | import ru.bozaro.gitlfs.common.data.Ref
7 | import java.io.IOException
8 |
9 | class ClientLocksTest {
10 | @Test
11 | @Throws(IOException::class, LockConflictException::class)
12 | fun simple() {
13 | val ref = Ref.create("refs/heads/master")
14 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/locking-01.yml")
15 | val client = Client(FakeAuthProvider(false), replay)
16 | val lock = client.lock("build.gradle", ref)
17 | Assert.assertNotNull(lock)
18 | Assert.assertNotNull(lock.id)
19 | Assert.assertEquals(lock.path, "build.gradle")
20 | try {
21 | client.lock("build.gradle", ref)
22 | Assert.fail()
23 | } catch (e: LockConflictException) {
24 | Assert.assertEquals(e.lock.id, lock.id)
25 | }
26 | run {
27 | val locks = client.listLocks("build.gradle", null, ref)
28 | Assert.assertEquals(locks.size, 1)
29 | Assert.assertEquals(locks[0].id, lock.id)
30 | }
31 | run {
32 | val locks = client.verifyLocks(ref)
33 | Assert.assertEquals(locks.ourLocks.size, 2)
34 | Assert.assertEquals(locks.ourLocks[1].id, lock.id)
35 | Assert.assertEquals(locks.theirLocks.size, 0)
36 | }
37 | val unlock = client.unlock(lock.id, true, ref)
38 | Assert.assertNotNull(unlock)
39 | Assert.assertEquals(unlock!!.id, lock.id)
40 | Assert.assertNull(client.unlock(lock.id, false, ref))
41 | replay.close()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import com.google.common.io.ByteStreams
4 | import ru.bozaro.gitlfs.client.AuthHelper.create
5 | import ru.bozaro.gitlfs.client.io.FileStreamProvider
6 | import ru.bozaro.gitlfs.common.data.Meta
7 | import java.io.File
8 | import java.io.IOException
9 | import java.io.InputStream
10 | import java.util.concurrent.Executors
11 |
12 | /**
13 | * This class provides examples for documentation.
14 | */
15 | class Examples {
16 | @Throws(IOException::class)
17 | fun download() {
18 | // tag::download[]
19 | val auth = create("git@github.com:foo/bar.git")
20 | val client = Client(auth)
21 |
22 | // Single object
23 | val content = client.getObject("4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") { `in`: InputStream -> ByteStreams.toByteArray(`in`) }
24 |
25 | // Batch mode
26 | val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
27 | val downloader = BatchDownloader(client, pool)
28 | val future = downloader.download(Meta("4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393", 10)) { `in`: InputStream -> ByteStreams.toByteArray(`in`) }
29 | // end::download[]
30 | }
31 |
32 | @Throws(Exception::class)
33 | fun upload() {
34 | // tag::upload[]
35 | val auth = create("git@github.com:foo/bar.git")
36 | val client = Client(auth)
37 |
38 | // Single object
39 | client.putObject(FileStreamProvider(File("foo.bin")))
40 |
41 | // Batch mode
42 | val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
43 | val uploader = BatchUploader(client, pool)
44 | val future = uploader.upload(FileStreamProvider(File("bar.bin")))
45 | // end::upload[]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/FakeAuthProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import com.google.common.collect.ImmutableMap
4 | import ru.bozaro.gitlfs.client.auth.AuthProvider
5 | import ru.bozaro.gitlfs.common.data.Link
6 | import ru.bozaro.gitlfs.common.data.Operation
7 | import java.net.URI
8 | import java.util.concurrent.atomic.AtomicInteger
9 |
10 | /**
11 | * Fake authenticator.
12 | *
13 | * @author Artem V. Navrotskiy
14 | */
15 | class FakeAuthProvider(private val chunkedUpload: Boolean) : AuthProvider {
16 | private val lock = Any()
17 |
18 | private val id = AtomicInteger(0)
19 | private var auth: Array? = null
20 |
21 | override fun getAuth(operation: Operation): Link {
22 | synchronized(lock) {
23 | if (auth == null) {
24 | auth = createAuth()
25 | }
26 | return auth!![operation.ordinal]
27 | }
28 | }
29 |
30 | override fun invalidateAuth(operation: Operation, auth: Link) {
31 | synchronized(lock) {
32 | if (this.auth != null && (this.auth!![0] == auth || this.auth!![1] == auth)) {
33 | this.auth = null
34 | }
35 | }
36 | }
37 |
38 | private fun createAuth(): Array {
39 | val uri = URI.create("http://gitlfs.local/test.git/info/lfs")
40 | val headers = ImmutableMap.builder()
41 | .put("Authorization", "RemoteAuth Token-" + id.incrementAndGet())
42 | val downloadAuth = Link(uri, headers.build(), null)
43 | if (chunkedUpload) {
44 | headers.put("Transfer-Encoding", "chunked")
45 | }
46 | val uploadAuth = Link(uri, headers.build(), null)
47 | return arrayOf(downloadAuth, uploadAuth)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpRecord.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import com.google.common.base.Utf8
4 | import com.google.common.io.BaseEncoding
5 | import org.apache.http.HttpEntityEnclosingRequest
6 | import org.apache.http.HttpResponse
7 | import org.apache.http.ProtocolVersion
8 | import org.apache.http.client.methods.CloseableHttpResponse
9 | import org.apache.http.client.methods.HttpUriRequest
10 | import org.apache.http.entity.ByteArrayEntity
11 | import org.apache.http.message.BasicHttpResponse
12 | import org.apache.http.protocol.HTTP
13 | import java.io.ByteArrayOutputStream
14 | import java.nio.charset.StandardCharsets
15 | import java.util.*
16 |
17 | /**
18 | * HTTP request-response pair for testing.
19 | *
20 | * @author Artem V. Navrotskiy
21 | */
22 | class HttpRecord {
23 | val request: Request
24 |
25 | val response: Response
26 |
27 | constructor(request: HttpUriRequest, response: HttpResponse) {
28 | this.request = Request(request)
29 | this.response = Response(response)
30 | }
31 |
32 | private constructor() {
33 | request = Request()
34 | response = Response()
35 | }
36 |
37 | class Response {
38 | private val statusCode: Int
39 | private val statusText: String
40 | private val headers: TreeMap
41 | private val body: ByteArray?
42 |
43 | internal constructor() {
44 | statusCode = 0
45 | statusText = ""
46 | headers = TreeMap()
47 | body = null
48 | }
49 |
50 | internal constructor(response: HttpResponse) {
51 | statusCode = response.statusLine.statusCode
52 | statusText = response.statusLine.reasonPhrase
53 | headers = TreeMap()
54 | for (header in response.allHeaders) {
55 | headers[header.name] = header.value
56 | }
57 | ByteArrayOutputStream().use { stream ->
58 | response.entity.writeTo(stream)
59 | body = stream.toByteArray()
60 | response.entity = ByteArrayEntity(body)
61 | }
62 | }
63 |
64 | fun toHttpResponse(): CloseableHttpResponse {
65 | val response = CloseableBasicHttpResponse(ProtocolVersion("HTTP", 1, 0), statusCode, statusText)
66 | for ((key, value) in headers) response.addHeader(key, value)
67 | if (body != null) response.entity = ByteArrayEntity(body)
68 | return response
69 | }
70 |
71 | override fun toString(): String {
72 | val sb = StringBuilder()
73 | sb.append("HTTP/1.0 ").append(statusCode).append(" ").append(statusText).append("\n")
74 | for ((key, value) in headers) {
75 | sb.append(key).append(": ").append(value).append("\n")
76 | }
77 | if (body != null) {
78 | sb.append("\n").append(asString(body))
79 | }
80 | return sb.toString()
81 | }
82 | }
83 |
84 | private class CloseableBasicHttpResponse(
85 | ver: ProtocolVersion,
86 | code: Int,
87 | reason: String
88 | ) : BasicHttpResponse(ver, code, reason), CloseableHttpResponse {
89 | override fun close() {
90 | // noop
91 | }
92 | }
93 |
94 | class Request {
95 | private val href: String
96 | private val method: String
97 | private val headers: TreeMap
98 | private val body: ByteArray?
99 |
100 | internal constructor() {
101 | href = ""
102 | method = ""
103 | headers = TreeMap()
104 | body = null
105 | }
106 |
107 | internal constructor(request: HttpUriRequest) {
108 | href = request.uri.toString()
109 | method = request.method
110 | headers = TreeMap()
111 | val entityRequest = if (request is HttpEntityEnclosingRequest) request else null
112 | val entity = entityRequest?.entity
113 | if (entity != null) {
114 | if (entity.isChunked || entity.contentLength < 0) {
115 | request.addHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING)
116 | } else {
117 | request.addHeader(HTTP.CONTENT_LEN, entity.contentLength.toString())
118 | }
119 | val contentType = entity.contentType
120 | if (contentType != null) {
121 | headers[contentType.name] = contentType.value
122 | }
123 | ByteArrayOutputStream().use { buffer ->
124 | entity.writeTo(buffer)
125 | body = buffer.toByteArray()
126 | }
127 | entityRequest.entity = ByteArrayEntity(body)
128 | } else {
129 | body = null
130 | }
131 | for (header in request.allHeaders) {
132 | headers[header.name] = header.value
133 | }
134 | headers.remove(HTTP.TARGET_HOST)
135 | headers.remove(HTTP.USER_AGENT)
136 | }
137 |
138 | override fun toString(): String {
139 | val sb = StringBuilder()
140 | sb.append(method).append(" ").append(href).append("\n")
141 | for ((key, value) in headers) {
142 | sb.append(key).append(": ").append(value).append("\n")
143 | }
144 | if (body != null) {
145 | sb.append("\n").append(asString(body))
146 | }
147 | return sb.toString()
148 | }
149 | }
150 |
151 | companion object {
152 | private fun asString(data: ByteArray): String {
153 | return if (Utf8.isWellFormed(data)) {
154 | String(data, StandardCharsets.UTF_8)
155 | } else {
156 | BaseEncoding.base16().encode(data)
157 | }
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpRecorder.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.apache.http.client.methods.CloseableHttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 | import java.io.IOException
6 |
7 | /**
8 | * Client with recording all request.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | class HttpRecorder(private val executor: HttpExecutor) : HttpExecutor {
13 | val records = ArrayList()
14 |
15 | @Throws(IOException::class)
16 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse {
17 | val response = executor.executeMethod(request)
18 | records.add(HttpRecord(request, response))
19 | return response
20 | }
21 |
22 | @Throws(IOException::class)
23 | override fun close() {
24 | executor.close()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpReplay.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.apache.http.client.methods.CloseableHttpResponse
4 | import org.apache.http.client.methods.HttpUriRequest
5 | import org.testng.Assert
6 | import java.io.IOException
7 | import java.util.*
8 |
9 | /**
10 | * Replay recorded HTTP requests.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class HttpReplay(records: List) : HttpExecutor {
15 | private val records: Deque
16 |
17 | @Throws(IOException::class)
18 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse {
19 | val record = records.pollFirst()
20 | Assert.assertNotNull(record)
21 | val expected = record.request.toString()
22 | val actual = HttpRecord.Request(request).toString()
23 | Assert.assertEquals(actual, expected)
24 | return record.response.toHttpResponse()
25 | }
26 |
27 | override fun close() {
28 | Assert.assertTrue(records.isEmpty())
29 | }
30 |
31 | init {
32 | this.records = ArrayDeque(records)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Recorder.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.apache.http.impl.client.HttpClients
4 | import ru.bozaro.gitlfs.client.AuthHelper.create
5 | import ru.bozaro.gitlfs.client.internal.HttpClientExecutor
6 | import ru.bozaro.gitlfs.common.data.BatchReq
7 | import ru.bozaro.gitlfs.common.data.Meta
8 | import ru.bozaro.gitlfs.common.data.Operation
9 | import java.io.File
10 | import java.io.FileOutputStream
11 | import java.io.IOException
12 | import java.io.OutputStreamWriter
13 | import java.nio.charset.StandardCharsets
14 |
15 | /**
16 | * Simple code for recording replay.
17 | *
18 | * @author Artem V. Navrotskiy
19 | */
20 | object Recorder {
21 | @Throws(IOException::class)
22 | @JvmStatic
23 | fun main(args: Array) {
24 | val auth = create("git@github.com:bozaro/test.git")
25 | HttpClients.createDefault().use { httpClient ->
26 | val recorder = HttpRecorder(HttpClientExecutor(httpClient))
27 | doWork(Client(auth, recorder))
28 | val yaml = YamlHelper.get()
29 | val file = File("build/replay.yml")
30 | file.parentFile.mkdirs()
31 | FileOutputStream(file).use { replay ->
32 | yaml.dumpAll(
33 | recorder.records.iterator(),
34 | OutputStreamWriter(replay, StandardCharsets.UTF_8)
35 | )
36 | }
37 | }
38 | }
39 |
40 | @Throws(IOException::class)
41 | private fun doWork(client: Client) {
42 | client.postBatch(
43 | BatchReq(
44 | Operation.Upload,
45 | listOf(
46 | Meta("b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 28),
47 | Meta("1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 3)
48 | )
49 | )
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlConstructor.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.yaml.snakeyaml.constructor.AbstractConstruct
4 | import org.yaml.snakeyaml.constructor.Constructor
5 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder
6 | import org.yaml.snakeyaml.nodes.Node
7 | import org.yaml.snakeyaml.nodes.ScalarNode
8 | import org.yaml.snakeyaml.nodes.Tag
9 | import java.nio.charset.StandardCharsets
10 |
11 | /**
12 | * Constructor for more user-friendly serializing request body.
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | class YamlConstructor : Constructor() {
17 | private inner class ConstructBlob : AbstractConstruct() {
18 | override fun construct(node: Node): Any {
19 | val value = constructScalar(node as ScalarNode)
20 | return value.toByteArray(StandardCharsets.UTF_8)
21 | }
22 | }
23 |
24 | private inner class ConstructBinary : AbstractConstruct() {
25 | override fun construct(node: Node): Any {
26 | val value = constructScalar(node as ScalarNode)
27 | return Base64Coder.decodeLines(value)
28 | }
29 | }
30 |
31 | init {
32 | yamlConstructors[Tag("!text")] = ConstructBlob()
33 | yamlConstructors[Tag.BINARY] = ConstructBinary()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlHelper.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import org.yaml.snakeyaml.DumperOptions
4 | import org.yaml.snakeyaml.Yaml
5 | import org.yaml.snakeyaml.introspector.BeanAccess
6 |
7 | /**
8 | * Helper for YAML.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | object YamlHelper {
13 | private val YAML = createYaml()
14 |
15 | private fun createYaml(): Yaml {
16 | val options = DumperOptions()
17 | options.lineBreak = DumperOptions.LineBreak.UNIX
18 | options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
19 | val yaml = Yaml(YamlConstructor(), YamlRepresenter(), options)
20 | yaml.setBeanAccess(BeanAccess.FIELD)
21 | return yaml
22 | }
23 |
24 | fun createReplay(resource: String): HttpReplay {
25 | val records: MutableList = ArrayList()
26 | for (item in get().loadAll(YamlHelper::class.java.getResourceAsStream(resource))) {
27 | records.add(item as HttpRecord)
28 | }
29 | return HttpReplay(records)
30 | }
31 |
32 | fun get(): Yaml {
33 | return YAML
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlRepresenter.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.client
2 |
3 | import com.google.common.base.Utf8
4 | import org.yaml.snakeyaml.DumperOptions
5 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder
6 | import org.yaml.snakeyaml.nodes.Node
7 | import org.yaml.snakeyaml.nodes.Tag
8 | import org.yaml.snakeyaml.representer.Represent
9 | import org.yaml.snakeyaml.representer.Representer
10 | import java.nio.charset.StandardCharsets
11 |
12 | /**
13 | * Representer for more user-friendly serializing request body.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class YamlRepresenter : Representer() {
18 | private inner class RepresentBlob : Represent {
19 | override fun representData(data: Any): Node {
20 | val value = data as ByteArray
21 | return if (Utf8.isWellFormed(value)) {
22 | representScalar(Tag("!text"), String(value, StandardCharsets.UTF_8), DumperOptions.ScalarStyle.LITERAL)
23 | } else {
24 | val binary = Base64Coder.encodeLines(data)
25 | representScalar(Tag.BINARY, binary, DumperOptions.ScalarStyle.LITERAL)
26 | }
27 | }
28 | }
29 |
30 | init {
31 | representers[ByteArray::class.java] = RepresentBlob()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-download-01.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "operation" : "download",
6 | "objects" : [ {
7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
8 | "size" : 28
9 | }, {
10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
11 | "size" : 3
12 | } ]
13 | }
14 | headers:
15 | Accept: application/vnd.git-lfs+json
16 | Authorization: RemoteAuth Token-1
17 | Content-Length: '253'
18 | Content-Type: application/vnd.git-lfs+json
19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch
20 | method: POST
21 | response:
22 | body: !text |-
23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"actions":{"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"Token-2","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151007T190640Z"}}}},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"error":{"code":404,"message":"Object does not exist on the server"}}]}
24 | headers:
25 | Access-Control-Allow-Credentials: 'true'
26 | Access-Control-Allow-Origin: '*'
27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
28 | Cache-Control: private, max-age=60, s-maxage=60
29 | Content-Length: '796'
30 | Content-Security-Policy: default-src 'none'
31 | Content-Type: application/json; charset=utf-8
32 | Date: Wed, 07 Oct 2015 19:06:40 GMT
33 | ETag: '"004aac442b3b84c52ed623c5c9e9f90a"'
34 | Server: GitHub.com
35 | Status: 200 OK
36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
37 | Vary: Accept-Encoding
38 | X-Content-Type-Options: nosniff
39 | X-Frame-Options: deny
40 | X-GitHub-Media-Type: unknown
41 | X-GitHub-Request-Id: 4FA5AFEC:10755:147CE471:56156D40
42 | X-RateLimit-Limit: '3000'
43 | X-RateLimit-Remaining: '2999'
44 | X-RateLimit-Reset: '1444244860'
45 | X-Served-By: 8a5c38021a5cd7cef7b8f49a296fee40
46 | X-XSS-Protection: 1; mode=block
47 | statusCode: 200
48 | statusText: OK
49 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-download-02.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "operation" : "download",
6 | "objects" : [ {
7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
8 | "size" : 28
9 | }, {
10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
11 | "size" : 3
12 | } ]
13 | }
14 | headers:
15 | Accept: application/vnd.git-lfs+json
16 | Authorization: RemoteAuth Token-1
17 | Content-Length: '253'
18 | Content-Type: application/vnd.git-lfs+json
19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch
20 | method: POST
21 | response:
22 | body: !text |-
23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"_links":{"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"Token-2","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151007T190640Z"}}}},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"error":{"code":404,"message":"Object does not exist on the server"}}]}
24 | headers:
25 | Access-Control-Allow-Credentials: 'true'
26 | Access-Control-Allow-Origin: '*'
27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
28 | Cache-Control: private, max-age=60, s-maxage=60
29 | Content-Length: '796'
30 | Content-Security-Policy: default-src 'none'
31 | Content-Type: application/json; charset=utf-8
32 | Date: Wed, 07 Oct 2015 19:06:40 GMT
33 | ETag: '"004aac442b3b84c52ed623c5c9e9f90a"'
34 | Server: GitHub.com
35 | Status: 200 OK
36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
37 | Vary: Accept-Encoding
38 | X-Content-Type-Options: nosniff
39 | X-Frame-Options: deny
40 | X-GitHub-Media-Type: unknown
41 | X-GitHub-Request-Id: 4FA5AFEC:10755:147CE471:56156D40
42 | X-RateLimit-Limit: '3000'
43 | X-RateLimit-Remaining: '2999'
44 | X-RateLimit-Reset: '1444244860'
45 | X-Served-By: 8a5c38021a5cd7cef7b8f49a296fee40
46 | X-XSS-Protection: 1; mode=block
47 | statusCode: 200
48 | statusText: OK
49 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-01.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "operation" : "upload",
6 | "objects" : [ {
7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
8 | "size" : 28
9 | }, {
10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
11 | "size" : 3
12 | } ]
13 | }
14 | headers:
15 | Accept: application/vnd.git-lfs+json
16 | Authorization: RemoteAuth Token-1
17 | Content-Length: '251'
18 | Content-Type: application/vnd.git-lfs+json
19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch
20 | method: POST
21 | response:
22 | body: !text |-
23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"actions":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]}
24 | headers:
25 | Access-Control-Allow-Credentials: 'true'
26 | Access-Control-Allow-Origin: '*'
27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
28 | Cache-Control: private, max-age=60, s-maxage=60
29 | Content-Length: '989'
30 | Content-Security-Policy: default-src 'none'
31 | Content-Type: application/json; charset=utf-8
32 | Date: Wed, 07 Oct 2015 19:07:30 GMT
33 | ETag: '"4061c2b43579394fce28f86aaeef3502"'
34 | Server: GitHub.com
35 | Status: 200 OK
36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
37 | Vary: Accept-Encoding
38 | X-Content-Type-Options: nosniff
39 | X-Frame-Options: deny
40 | X-GitHub-Media-Type: unknown
41 | X-RateLimit-Limit: '3000'
42 | X-RateLimit-Remaining: '2998'
43 | X-RateLimit-Reset: '1444244860'
44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290
45 | X-XSS-Protection: 1; mode=block
46 | statusCode: 200
47 | statusText: OK
48 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-02.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "operation" : "upload",
6 | "objects" : [ {
7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
8 | "size" : 28
9 | }, {
10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
11 | "size" : 3
12 | } ]
13 | }
14 | headers:
15 | Accept: application/vnd.git-lfs+json
16 | Authorization: RemoteAuth Token-1
17 | Content-Length: '251'
18 | Content-Type: application/vnd.git-lfs+json
19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch
20 | method: POST
21 | response:
22 | body: !text |-
23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]}
24 | headers:
25 | Access-Control-Allow-Credentials: 'true'
26 | Access-Control-Allow-Origin: '*'
27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
28 | Cache-Control: private, max-age=60, s-maxage=60
29 | Content-Length: '989'
30 | Content-Security-Policy: default-src 'none'
31 | Content-Type: application/json; charset=utf-8
32 | Date: Wed, 07 Oct 2015 19:07:30 GMT
33 | ETag: '"4061c2b43579394fce28f86aaeef3502"'
34 | Server: GitHub.com
35 | Status: 200 OK
36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
37 | Vary: Accept-Encoding
38 | X-Content-Type-Options: nosniff
39 | X-Frame-Options: deny
40 | X-GitHub-Media-Type: unknown
41 | X-RateLimit-Limit: '3000'
42 | X-RateLimit-Remaining: '2998'
43 | X-RateLimit-Reset: '1444244860'
44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290
45 | X-XSS-Protection: 1; mode=block
46 | statusCode: 200
47 | statusText: OK
48 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-chunked.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "operation" : "upload",
6 | "objects" : [ {
7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
8 | "size" : 28
9 | }, {
10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
11 | "size" : 3
12 | } ]
13 | }
14 | headers:
15 | Accept: application/vnd.git-lfs+json
16 | Authorization: RemoteAuth Token-1
17 | Content-Type: application/vnd.git-lfs+json
18 | Transfer-Encoding: chunked
19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch
20 | method: POST
21 | response:
22 | body: !text |-
23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"actions":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]}
24 | headers:
25 | Access-Control-Allow-Credentials: 'true'
26 | Access-Control-Allow-Origin: '*'
27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
28 | Cache-Control: private, max-age=60, s-maxage=60
29 | Content-Length: '989'
30 | Content-Security-Policy: default-src 'none'
31 | Content-Type: application/json; charset=utf-8
32 | Date: Wed, 07 Oct 2015 19:07:30 GMT
33 | ETag: '"4061c2b43579394fce28f86aaeef3502"'
34 | Server: GitHub.com
35 | Status: 200 OK
36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
37 | Vary: Accept-Encoding
38 | X-Content-Type-Options: nosniff
39 | X-Frame-Options: deny
40 | X-GitHub-Media-Type: unknown
41 | X-RateLimit-Limit: '3000'
42 | X-RateLimit-Remaining: '2998'
43 | X-RateLimit-Reset: '1444244860'
44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290
45 | X-XSS-Protection: 1; mode=block
46 | statusCode: 200
47 | statusText: OK
48 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-download-01.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: null
4 | headers:
5 | Accept: application/vnd.git-lfs+json
6 | Authorization: RemoteAuth Token-1
7 | href: http://gitlfs.local/test.git/info/lfs/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e
8 | method: GET
9 | response:
10 | body: !text |-
11 | {"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"_links":{"self":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e"},"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151002T185112Z"}}}}
12 | headers:
13 | Accept: application/vnd.git-lfs+json
14 | Authorization: RemoteAuth Token-1
15 | Host: api.github.com
16 | User-Agent: Jakarta Commons-HttpClient/3.1
17 | statusCode: 200
18 | statusText: OK
19 | --- !!ru.bozaro.gitlfs.client.HttpRecord
20 | request:
21 | body: null
22 | headers:
23 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9
24 | x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
25 | x-amz-date: 20151002T185112Z
26 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138
27 | method: GET
28 | response:
29 | body: !text |-
30 | Fri Oct 02 21:07:33 MSK 2015
31 | headers:
32 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9
33 | Host: github-cloud.s3.amazonaws.com
34 | User-Agent: Jakarta Commons-HttpClient/3.1
35 | x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
36 | x-amz-date: 20151002T185112Z
37 | statusCode: 200
38 | statusText: OK
39 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-download-02.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: null
4 | headers:
5 | Accept: application/vnd.git-lfs+json
6 | Authorization: RemoteAuth Token-1
7 | href: http://gitlfs.local/test.git/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
8 | method: GET
9 | response:
10 | body: !text |-
11 | {"message":"Not Found","documentation_url":"https://github.com/contact"}
12 | headers:
13 | Access-Control-Allow-Credentials: 'true'
14 | Access-Control-Allow-Origin: '*'
15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
16 | Content-Length: '72'
17 | Content-Security-Policy: default-src 'none'
18 | Content-Type: application/json; charset=utf-8
19 | Date: Wed, 07 Oct 2015 04:06:04 GMT
20 | Server: GitHub.com
21 | Status: 404 Not Found
22 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
23 | X-Content-Type-Options: nosniff
24 | X-Frame-Options: deny
25 | X-GitHub-Media-Type: unknown
26 | X-GitHub-Request-Id: 4FA5AFEC:17F56:60D2406:56149A2C
27 | X-RateLimit-Limit: '3000'
28 | X-RateLimit-Remaining: '2999'
29 | X-RateLimit-Reset: '1444190824'
30 | X-XSS-Protection: 1; mode=block
31 | statusCode: 404
32 | statusText: Not Found
33 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-01.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
6 | "size" : 28
7 | }
8 | headers:
9 | Accept: application/vnd.git-lfs+json
10 | Authorization: RemoteAuth Token-1
11 | Content-Length: '95'
12 | Content-Type: application/vnd.git-lfs+json
13 | href: http://gitlfs.local/test.git/info/lfs/objects
14 | method: POST
15 | response:
16 | body: !text |-
17 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0","x-amz-content-sha256":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","x-amz-date":"20151002T180739Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify","header":{"Authorization":"RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==","Accept":"application/vnd.git-lfs+json"}}}}
18 | headers:
19 | Accept: application/vnd.git-lfs+json
20 | Authorization: RemoteAuth 40a488f3-dd0e-4f61-862b-10350d273b7e
21 | Content-Length: '95'
22 | Content-Type: application/vnd.git-lfs+json
23 | Host: api.github.com
24 | User-Agent: Jakarta Commons-HttpClient/3.1
25 | statusCode: 202
26 | statusText: Accepted
27 | --- !!ru.bozaro.gitlfs.client.HttpRecord
28 | request:
29 | body: !text |-
30 | Fri Oct 02 21:07:33 MSK 2015
31 | headers:
32 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0
33 | Content-Length: '28'
34 | Content-Type: application/octet-stream
35 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e
36 | x-amz-date: 20151002T180739Z
37 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138
38 | method: PUT
39 | response:
40 | body: !text ""
41 | headers:
42 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0
43 | Content-Length: '28'
44 | Content-Type: application/octet-stream
45 | Host: github-cloud.s3.amazonaws.com
46 | User-Agent: Jakarta Commons-HttpClient/3.1
47 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e
48 | x-amz-date: 20151002T180739Z
49 | statusCode: 200
50 | statusText: OK
51 | --- !!ru.bozaro.gitlfs.client.HttpRecord
52 | request:
53 | body: !text |-
54 | {
55 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
56 | "size" : 28
57 | }
58 | headers:
59 | Accept: application/vnd.git-lfs+json
60 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==
61 | Content-Length: '95'
62 | Content-Type: application/vnd.git-lfs+json
63 | href: https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify
64 | method: POST
65 | response:
66 | body: !text |-
67 | {}
68 | headers:
69 | Accept: application/vnd.git-lfs+json
70 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==
71 | Content-Length: '95'
72 | Content-Type: application/vnd.git-lfs+json
73 | Host: api.github.com
74 | User-Agent: Jakarta Commons-HttpClient/3.1
75 | statusCode: 200
76 | statusText: OK
77 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-02.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
6 | "size" : 28
7 | }
8 | headers:
9 | Accept: application/vnd.git-lfs+json
10 | Authorization: RemoteAuth Token-1
11 | Content-Length: '95'
12 | Content-Type: application/vnd.git-lfs+json
13 | href: http://gitlfs.local/test.git/info/lfs/objects
14 | method: POST
15 | response:
16 | body: !text |-
17 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"}
18 | headers:
19 | Accept: application/vnd.git-lfs+json
20 | Authorization: RemoteAuth Token-1
21 | Content-Length: '95'
22 | Content-Type: application/vnd.git-lfs+json
23 | Host: api.github.com
24 | User-Agent: Jakarta Commons-HttpClient/3.1
25 | statusCode: 403
26 | statusText: Forbidden
27 | --- !!ru.bozaro.gitlfs.client.HttpRecord
28 | request:
29 | body: !text |-
30 | {
31 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
32 | "size" : 28
33 | }
34 | headers:
35 | Accept: application/vnd.git-lfs+json
36 | Authorization: RemoteAuth Token-2
37 | Content-Length: '95'
38 | Content-Type: application/vnd.git-lfs+json
39 | href: http://gitlfs.local/test.git/info/lfs/objects
40 | method: POST
41 | response:
42 | body: !text |-
43 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"}
44 | headers:
45 | Accept: application/vnd.git-lfs+json
46 | Authorization: RemoteAuth Token-2
47 | Content-Length: '95'
48 | Content-Type: application/vnd.git-lfs+json
49 | Host: api.github.com
50 | User-Agent: Jakarta Commons-HttpClient/3.1
51 | statusCode: 403
52 | statusText: Forbidden
53 | --- !!ru.bozaro.gitlfs.client.HttpRecord
54 | request:
55 | body: !text |-
56 | {
57 | "oid" : "e6ba93fef485f9cb6748286d3ad610123bcf37f6ceb8a0f847a5d4f20ed31af3",
58 | "size" : 28
59 | }
60 | headers:
61 | Accept: application/vnd.git-lfs+json
62 | Authorization: RemoteAuth Token-3
63 | Content-Length: '95'
64 | Content-Type: application/vnd.git-lfs+json
65 | href: https://api.github.com/lfs/bozaro/test/objects
66 | method: POST
67 | response:
68 | body: !text |-
69 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"}
70 | headers:
71 | Accept: application/vnd.git-lfs+json
72 | Authorization: RemoteAuth Token-3
73 | Content-Length: '95'
74 | Content-Type: application/vnd.git-lfs+json
75 | Host: api.github.com
76 | User-Agent: Jakarta Commons-HttpClient/3.1
77 | statusCode: 403
78 | statusText: Forbidden
79 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-03.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
6 | "size" : 28
7 | }
8 | headers:
9 | Accept: application/vnd.git-lfs+json
10 | Authorization: RemoteAuth Token-1
11 | Content-Length: '95'
12 | Content-Type: application/vnd.git-lfs+json
13 | href: http://gitlfs.local/test.git/info/lfs/objects
14 | method: POST
15 | response:
16 | body: !text |-
17 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"}
18 | headers:
19 | Accept: application/vnd.git-lfs+json
20 | Authorization: RemoteAuth Token-1
21 | Content-Length: '95'
22 | Content-Type: application/vnd.git-lfs+json
23 | Host: api.github.com
24 | User-Agent: Jakarta Commons-HttpClient/3.1
25 | statusCode: 403
26 | statusText: Forbidden
27 | --- !!ru.bozaro.gitlfs.client.HttpRecord
28 | request:
29 | body: !text |-
30 | {
31 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
32 | "size" : 28
33 | }
34 | headers:
35 | Accept: application/vnd.git-lfs+json
36 | Authorization: RemoteAuth Token-2
37 | Content-Length: '95'
38 | Content-Type: application/vnd.git-lfs+json
39 | href: http://gitlfs.local/test.git/info/lfs/objects
40 | method: POST
41 | response:
42 | body: !text |-
43 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0","x-amz-content-sha256":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","x-amz-date":"20151002T180739Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify","header":{"Authorization":"RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==","Accept":"application/vnd.git-lfs+json"}}}}
44 | headers:
45 | Accept: application/vnd.git-lfs+json
46 | Authorization: RemoteAuth 40a488f3-dd0e-4f61-862b-10350d273b7e
47 | Content-Length: '95'
48 | Content-Type: application/vnd.git-lfs+json
49 | Host: api.github.com
50 | User-Agent: Jakarta Commons-HttpClient/3.1
51 | statusCode: 202
52 | statusText: Accepted
53 | --- !!ru.bozaro.gitlfs.client.HttpRecord
54 | request:
55 | body: !text |-
56 | Fri Oct 02 21:07:33 MSK 2015
57 | headers:
58 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0
59 | Content-Length: '28'
60 | Content-Type: application/octet-stream
61 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e
62 | x-amz-date: 20151002T180739Z
63 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138
64 | method: PUT
65 | response:
66 | body: !text ""
67 | headers:
68 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0
69 | Content-Length: '28'
70 | Content-Type: application/octet-stream
71 | Host: github-cloud.s3.amazonaws.com
72 | User-Agent: Jakarta Commons-HttpClient/3.1
73 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e
74 | x-amz-date: 20151002T180739Z
75 | statusCode: 200
76 | statusText: OK
77 | --- !!ru.bozaro.gitlfs.client.HttpRecord
78 | request:
79 | body: !text |-
80 | {
81 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e",
82 | "size" : 28
83 | }
84 | headers:
85 | Accept: application/vnd.git-lfs+json
86 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==
87 | Content-Length: '95'
88 | Content-Type: application/vnd.git-lfs+json
89 | href: https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify
90 | method: POST
91 | response:
92 | body: !text |-
93 | {}
94 | headers:
95 | Accept: application/vnd.git-lfs+json
96 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==
97 | Content-Length: '95'
98 | Content-Type: application/vnd.git-lfs+json
99 | Host: api.github.com
100 | User-Agent: Jakarta Commons-HttpClient/3.1
101 | statusCode: 200
102 | statusText: OK
103 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-04.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "oid" : "61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308",
6 | "size" : 15
7 | }
8 | headers:
9 | Accept: application/vnd.git-lfs+json
10 | Authorization: RemoteAuth Token-1
11 | Content-Length: '95'
12 | Content-Type: application/vnd.git-lfs+json
13 | href: http://gitlfs.local/test.git/info/lfs/objects
14 | method: POST
15 | response:
16 | body: !text |-
17 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/61/f2/61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151003/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=21bf6f6e7c966d514de3c6cc6cfaa5ffcba218337658674b7f0889337b7f11c7","x-amz-content-sha256":"61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308","x-amz-date":"20151003T072433Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308/verify","header":{"Authorization":"RemoteAuth ACWCGjaSQ5a9uvMqIQ9ZGyaphLde-j3sks5WENQxwA==","Accept":"application/vnd.git-lfs+json"}}}}
18 | headers:
19 | Accept: application/vnd.git-lfs+json
20 | Authorization: RemoteAuth 07ff548d-8797-442e-bedf-b03f8c967091
21 | Content-Length: '95'
22 | Content-Type: application/vnd.git-lfs+json
23 | Host: api.github.com
24 | User-Agent: Jakarta Commons-HttpClient/3.1
25 | statusCode: 200
26 | statusText: OK
27 |
--------------------------------------------------------------------------------
/gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/locking-01.yml:
--------------------------------------------------------------------------------
1 | !!ru.bozaro.gitlfs.client.HttpRecord
2 | request:
3 | body: !text |-
4 | {
5 | "path" : "build.gradle",
6 | "ref" : {
7 | "name" : "refs/heads/master"
8 | }
9 | }
10 | headers:
11 | Accept: application/vnd.git-lfs+json
12 | Authorization: RemoteAuth Token-1
13 | Content-Length: '79'
14 | Content-Type: application/vnd.git-lfs+json
15 | href: http://gitlfs.local/test.git/info/lfs/locks
16 | method: POST
17 | response:
18 | body: !text |
19 | {"lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:27.933646398Z"}}
20 | headers:
21 | Content-Length: '92'
22 | Content-Type: application/vnd.git-lfs+json
23 | Date: Wed, 15 May 2019 09:56:27 GMT
24 | X-GitHub-Request-Id: D6AC:50EC:37071C:6CF18D:5CDBE24B
25 | statusCode: 201
26 | statusText: Created
27 | --- !!ru.bozaro.gitlfs.client.HttpRecord
28 | request:
29 | body: !text |-
30 | {
31 | "path" : "build.gradle",
32 | "ref" : {
33 | "name" : "refs/heads/master"
34 | }
35 | }
36 | headers:
37 | Accept: application/vnd.git-lfs+json
38 | Authorization: RemoteAuth Token-1
39 | Content-Length: '79'
40 | Content-Type: application/vnd.git-lfs+json
41 | href: http://gitlfs.local/test.git/info/lfs/locks
42 | method: POST
43 | response:
44 | body: !text |
45 | {"documentation_url":"https://github.com/contact","lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:28Z"},"message":"Lock exists","request_id":"D6AC:50EC:370726:6CF1AA:5CDBE24B"}
46 | headers:
47 | Content-Length: '203'
48 | Content-Type: application/vnd.git-lfs+json
49 | Date: Wed, 15 May 2019 09:56:28 GMT
50 | X-GitHub-Request-Id: D6AC:50EC:370726:6CF1AA:5CDBE24B
51 | statusCode: 409
52 | statusText: Conflict
53 | --- !!ru.bozaro.gitlfs.client.HttpRecord
54 | request:
55 | body: null
56 | headers:
57 | Accept: application/vnd.git-lfs+json
58 | Authorization: RemoteAuth Token-1
59 | href: http://gitlfs.local/test.git/info/lfs/locks?path=build.gradle&refspec=refs%2Fheads%2Fmaster
60 | method: GET
61 | response:
62 | body: !text |
63 | {"locks":[{"id":"107214","path":"build.gradle","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:56:28Z"}],"next_cursor":""}
64 | headers:
65 | Content-Length: '134'
66 | Content-Type: application/vnd.git-lfs+json
67 | Date: Wed, 15 May 2019 09:56:28 GMT
68 | X-GitHub-Request-Id: D6AC:50EC:370759:6CF1CF:5CDBE24C
69 | statusCode: 200
70 | statusText: OK
71 | --- !!ru.bozaro.gitlfs.client.HttpRecord
72 | request:
73 | body: !text |-
74 | {
75 | "ref" : {
76 | "name" : "refs/heads/master"
77 | }
78 | }
79 | headers:
80 | Accept: application/vnd.git-lfs+json
81 | Authorization: RemoteAuth Token-1
82 | Content-Length: '52'
83 | Content-Type: application/vnd.git-lfs+json
84 | href: http://gitlfs.local/test.git/info/lfs/locks/verify
85 | method: POST
86 | response:
87 | body: !text |
88 | {"ours":[{"id":"107193","path":"README.md","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:21:33Z"},{"id":"107214","path":"build.gradle","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:56:28Z"}],"theirs":[],"next_cursor":""}
89 | headers:
90 | Content-Length: '247'
91 | Content-Type: application/vnd.git-lfs+json
92 | Date: Wed, 15 May 2019 09:56:28 GMT
93 | X-GitHub-Request-Id: D6AC:50EC:370768:6CF22D:5CDBE24C
94 | statusCode: 200
95 | statusText: OK
96 | --- !!ru.bozaro.gitlfs.client.HttpRecord
97 | request:
98 | body: !text |-
99 | {
100 | "force" : true,
101 | "ref" : {
102 | "name" : "refs/heads/master"
103 | }
104 | }
105 | headers:
106 | Accept: application/vnd.git-lfs+json
107 | Authorization: RemoteAuth Token-1
108 | Content-Length: '70'
109 | Content-Type: application/vnd.git-lfs+json
110 | href: http://gitlfs.local/test.git/info/lfs/locks/107214/unlock
111 | method: POST
112 | response:
113 | body: !text |
114 | {"lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:28Z","unlocked_at":"2019-05-15T09:56:29.09800075Z"}}
115 | headers:
116 | Content-Length: '128'
117 | Content-Type: application/vnd.git-lfs+json
118 | Date: Wed, 15 May 2019 09:56:29 GMT
119 | X-GitHub-Request-Id: D6AC:50EC:370779:6CF25A:5CDBE24C
120 | statusCode: 200
121 | statusText: OK
122 | --- !!ru.bozaro.gitlfs.client.HttpRecord
123 | request:
124 | body: !text |-
125 | {
126 | "force" : false,
127 | "ref" : {
128 | "name" : "refs/heads/master"
129 | }
130 | }
131 | headers:
132 | Accept: application/vnd.git-lfs+json
133 | Authorization: RemoteAuth Token-1
134 | Content-Length: '71'
135 | Content-Type: application/vnd.git-lfs+json
136 | href: http://gitlfs.local/test.git/info/lfs/locks/107214/unlock
137 | method: POST
138 | response:
139 | body: !text |
140 | {"documentation_url":"https://github.com/contact","message":"Lock not found","request_id":"D6AC:50EC:370786:6CF26F:5CDBE24D"}
141 | headers:
142 | Content-Length: '126'
143 | Content-Type: application/vnd.git-lfs+json
144 | Date: Wed, 15 May 2019 09:56:29 GMT
145 | X-GitHub-Request-Id: D6AC:50EC:370786:6CF26F:5CDBE24D
146 | statusCode: 404
147 | statusText: Not Found
148 |
--------------------------------------------------------------------------------
/gitlfs-common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | description = "Java Git-LFS common structures"
2 |
3 | dependencies {
4 | api("com.fasterxml.jackson.core:jackson-databind:2.16.1")
5 | }
6 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/Constants.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common
2 |
3 | /**
4 | * Git-lfs constants.
5 | *
6 | * @author Artem V. Navrotskiy
7 | */
8 | object Constants {
9 | /**
10 | * See [][HTTP/1.1 documentation"><a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">HTTP/1.1 documentation</a>.
11 | */
12 | const val HEADER_AUTHORIZATION = "Authorization"
13 |
14 | const val HEADER_ACCEPT = "Accept"
15 |
16 | const val HEADER_LOCATION = "Location"
17 |
18 | const val MIME_LFS_JSON = "application/vnd.git-lfs+json"
19 |
20 | const val MIME_BINARY = "application/octet-stream"
21 |
22 | const val PATH_OBJECTS = "objects"
23 |
24 | const val PATH_BATCH = "objects/batch"
25 |
26 | const val PATH_LOCKS = "locks"
27 |
28 | /**
29 | * Minimal supported batch size.
30 | */
31 | const val BATCH_SIZE = 100
32 | }
33 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/JsonHelper.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude
4 | import com.fasterxml.jackson.core.util.DefaultIndenter
5 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
6 | import com.fasterxml.jackson.databind.DeserializationFeature
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import com.fasterxml.jackson.databind.SerializationFeature
9 | import com.fasterxml.jackson.databind.util.StdDateFormat
10 | import java.text.DateFormat
11 |
12 | /**
13 | * Json utility class.
14 | *
15 | * @author Artem V. Navrotskiy
16 | * @author Marat Radchenko @slonopotamus.org>
17 | */
18 | object JsonHelper {
19 | /**
20 | * git-lfs cannot parse timezone without colon: [com.fasterxml.jackson.databind.util.StdDateFormat].
21 | *
22 | *
23 | * See https://github.com/git-lfs/git-lfs/issues/3660
24 | */
25 | val dateFormat: DateFormat = StdDateFormat.instance.withColonInTimeZone(true)
26 |
27 | val mapper = ObjectMapper()
28 |
29 | init {
30 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
31 | mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
32 | mapper.enable(SerializationFeature.INDENT_OUTPUT)
33 | mapper.dateFormat = dateFormat
34 |
35 | // By default, pretty printer uses system newline. Explicitly configure it to use \n
36 | mapper.setDefaultPrettyPrinter(
37 | DefaultPrettyPrinter(DefaultPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR)
38 | .withObjectIndenter(DefaultIndenter(" ", "\n")))
39 | mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/LockConflictException.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common
2 |
3 | import ru.bozaro.gitlfs.common.data.Lock
4 |
5 | class LockConflictException(message: String?, val lock: Lock) : Exception(message) {
6 |
7 | constructor(lock: Lock) : this("Lock exists", lock)
8 | }
9 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/VerifyLocksResult.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common
2 |
3 | import ru.bozaro.gitlfs.common.data.Lock
4 |
5 | class VerifyLocksResult(val ourLocks: List, val theirLocks: List)
6 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchItem.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 | import java.util.*
5 |
6 | /**
7 | * LFS batch object.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | class BatchItem(
12 | @JsonProperty(value = "oid", required = true) oid: String,
13 | @JsonProperty(value = "size", required = true) size: Long,
14 | @JsonProperty(value = "actions") links1: Map?,
15 | @JsonProperty(value = "_links") links2: Map?,
16 | @field:JsonProperty(value = "error")
17 | @param:JsonProperty(value = "error")
18 | val error: Error?
19 | ) : Meta(oid, size), Links {
20 | @JsonProperty(value = "actions")
21 | override val links: Map = combine(links1, links2)
22 |
23 | constructor(meta: Meta, links: MutableMap) : this(meta.oid, meta.size, links, null, null)
24 | constructor(meta: Meta, error: Error?) : this(meta.oid, meta.size, null, null, error)
25 |
26 | companion object {
27 | private fun combine(a: Map?, b: Map?): Map {
28 | var r: Map? = null
29 | if (a != null && a.isNotEmpty()) {
30 | r = a
31 | }
32 | if (b != null && b.isNotEmpty()) {
33 | if (r == null) {
34 | r = b
35 | } else {
36 | r = TreeMap(r)
37 | r.putAll(b)
38 | }
39 | }
40 | return if (r == null) emptyMap() else Collections.unmodifiableMap(r)
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchReq.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 | import java.util.*
5 |
6 | /**
7 | * Batch request.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | class BatchReq(
12 | @field:JsonProperty(value = "operation", required = true)
13 | @param:JsonProperty(value = "operation", required = true)
14 | val operation: Operation,
15 | @JsonProperty(value = "objects", required = true) objects: List
16 | ) {
17 |
18 | @JsonProperty(value = "objects", required = true)
19 | val objects: List = Collections.unmodifiableList(ArrayList(objects))
20 | }
21 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 | import java.util.*
6 |
7 | /**
8 | * Batch request.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | class BatchRes @JsonCreator constructor(
13 | @JsonProperty(value = "objects", required = true) objects: List
14 | ) {
15 | @JsonProperty(value = "objects", required = true)
16 | val objects: List = Collections.unmodifiableList(ArrayList(objects))
17 | }
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/CreateLockReq.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class CreateLockReq(
6 | /**
7 | * String path name of the locked file.
8 | */
9 | @field:JsonProperty(value = "path", required = true)
10 | @param:JsonProperty(value = "path", required = true) val path: String,
11 | /**
12 | * Optional object describing the server ref that the locks belong to.
13 | */
14 | @field:JsonProperty(value = "ref")
15 | @param:JsonProperty(value = "ref")
16 | val ref: Ref?
17 | )
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/CreateLockRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class CreateLockRes(
6 | @field:JsonProperty(value = "lock", required = true)
7 | @param:JsonProperty(value = "lock", required = true)
8 | val lock: Lock
9 | )
10 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockReq.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class DeleteLockReq(
6 | /**
7 | * Optional boolean specifying that the user is deleting another user's lock.
8 | */
9 | @field:JsonProperty(value = "force") @param:JsonProperty(value = "force") private val force: Boolean?,
10 | /**
11 | * Optional object describing the server ref that the locks belong to.
12 | */
13 | @field:JsonProperty(value = "ref") @param:JsonProperty(value = "ref") val ref: Ref?) {
14 | fun isForce(): Boolean {
15 | return force != null && force
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class DeleteLockRes(
6 | @field:JsonProperty(value = "lock", required = true)
7 | @param:JsonProperty(value = "lock", required = true)
8 | val lock: Lock
9 | )
10 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Error.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 |
6 | /**
7 | * LFS error description.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | class Error @JsonCreator constructor(
12 | @field:JsonProperty(value = "code", required = true) @param:JsonProperty(value = "code") val code: Int,
13 | @field:JsonProperty(value = "message") @param:JsonProperty(value = "message") val message: String?
14 | )
15 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Link.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 | import java.net.URI
6 | import java.util.*
7 |
8 | /**
9 | * LFS reference.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | class Link @JsonCreator constructor(
14 | @field:JsonProperty(value = "href", required = true)
15 | @param:JsonProperty(value = "href", required = true)
16 | val href: URI,
17 | @JsonProperty("header") header: Map?,
18 | @JsonProperty("expires_at") expiresAt: Date?
19 | ) {
20 |
21 | @JsonProperty("header")
22 | val header: Map = header?.let { TreeMap(it) } ?: emptyMap()
23 |
24 | @JsonProperty("expires_at")
25 | val expiresAt: Date? = if (expiresAt != null) Date(expiresAt.time) else null
26 | }
27 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LinkType.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonValue
5 |
6 | /**
7 | * LFSP operation type.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | enum class LinkType {
12 | Download {
13 | override fun toValue(): String {
14 | return "download"
15 | }
16 |
17 | override fun visit(visitor: Visitor): R {
18 | return visitor.visitDownload()
19 | }
20 | },
21 | Upload {
22 | override fun toValue(): String {
23 | return "upload"
24 | }
25 |
26 | override fun visit(visitor: Visitor): R {
27 | return visitor.visitUpload()
28 | }
29 | },
30 | Verify {
31 | override fun toValue(): String {
32 | return "verify"
33 | }
34 |
35 | override fun visit(visitor: Visitor): R {
36 | return visitor.visitVerify()
37 | }
38 | },
39 | Self {
40 | override fun toValue(): String {
41 | return "self"
42 | }
43 |
44 | override fun visit(visitor: Visitor): R {
45 | return visitor.visitSelf()
46 | }
47 | };
48 |
49 | @JsonValue
50 | abstract fun toValue(): String
51 | abstract fun visit(visitor: Visitor): R
52 | interface Visitor {
53 | fun visitDownload(): R
54 | fun visitUpload(): R
55 | fun visitVerify(): R
56 | fun visitSelf(): R
57 | }
58 |
59 | companion object {
60 | @JsonCreator
61 | fun forValue(value: String): LinkType? {
62 | for (item in values()) {
63 | if (item.toValue() == value) {
64 | return item
65 | }
66 | }
67 | return null
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Links.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | /**
4 | * Object locations.
5 | *
6 | * @author Artem V. Navrotskiy
7 | */
8 | interface Links {
9 | val links: Map
10 | }
11 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Lock.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 | import java.util.*
5 |
6 | class Lock(
7 | /**
8 | * String ID of the Lock.
9 | */
10 | @field:JsonProperty(value = "id", required = true)
11 | @param:JsonProperty(value = "id", required = true)
12 | val id: String,
13 | /**
14 | * String path name of the locked file.
15 | */
16 | @field:JsonProperty(value = "path", required = true)
17 | @param:JsonProperty(value = "path", required = true)
18 | val path: String,
19 | /**
20 | * The timestamp the lock was created, as an ISO 8601 formatted string.
21 | */
22 | @field:JsonProperty(value = "locked_at", required = true)
23 | @param:JsonProperty(value = "locked_at")
24 | val lockedAt: Date,
25 | /**
26 | * The name of the user that created the Lock. This should be set from the user credentials posted when creating the lock.
27 | */
28 | @field:JsonProperty(value = "owner")
29 | @param:JsonProperty(value = "owner")
30 | val owner: User?
31 | ) : Comparable {
32 |
33 | override fun hashCode(): Int {
34 | return Objects.hash(id)
35 | }
36 |
37 | override fun equals(other: Any?): Boolean {
38 | return if (other !is Lock) false else id == other.id
39 | }
40 |
41 | override fun compareTo(other: Lock): Int {
42 | return lockComparator.compare(this, other)
43 | }
44 |
45 | companion object {
46 | val lockComparator: Comparator = Comparator
47 | .comparing { obj: Lock -> obj.lockedAt }
48 | .thenComparing { obj: Lock -> obj.id }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LockConflictRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 |
6 | class LockConflictRes @JsonCreator constructor(
7 | @field:JsonProperty(value = "message", required = true)
8 | @param:JsonProperty(value = "message", required = true)
9 | val message: String,
10 | @field:JsonProperty(value = "lock", required = true)
11 | @param:JsonProperty(value = "lock", required = true)
12 | val lock: Lock
13 | )
14 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LocksRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class LocksRes(
6 | @field:JsonProperty(value = "locks", required = true)
7 | @param:JsonProperty(value = "locks", required = true)
8 | val locks: List,
9 | @field:JsonProperty(value = "next_cursor")
10 | @param:JsonProperty(value = "next_cursor")
11 | val nextCursor: String?
12 | )
13 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Meta.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 | import java.util.*
6 |
7 | /**
8 | * LFS object location.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | open class Meta @JsonCreator constructor(
13 | @field:JsonProperty(value = "oid", required = true)
14 | @param:JsonProperty(value = "oid", required = true) val oid: String,
15 | @field:JsonProperty(value = "size", required = true)
16 | @param:JsonProperty(value = "size", required = true) val size: Long
17 | ) {
18 |
19 | override fun hashCode(): Int {
20 | return Objects.hash(oid, size)
21 | }
22 |
23 | override fun equals(other: Any?): Boolean {
24 | if (other !is Meta) return false
25 | return size == other.size && oid == other.oid
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/ObjectRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 | import java.util.*
6 |
7 | /**
8 | * LFS object location.
9 | *
10 | * @author Artem V. Navrotskiy
11 | */
12 | class ObjectRes @JsonCreator constructor(
13 | @JsonProperty(value = "oid") oid: String?,
14 | @JsonProperty(value = "size") size: Long,
15 | @JsonProperty(value = "_links", required = true) links: Map
16 | ) : Links {
17 | @JsonProperty(value = "_links", required = true)
18 | override val links: Map = Collections.unmodifiableMap(TreeMap(links))
19 | val meta: Meta? = oid?.let { Meta(it, size) }
20 | }
21 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Operation.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator
4 | import com.fasterxml.jackson.annotation.JsonValue
5 |
6 | /**
7 | * LFSP operation type.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | enum class Operation {
12 | Download {
13 | override fun toValue(): String {
14 | return "download"
15 | }
16 |
17 | override fun visit(visitor: Visitor): R {
18 | return visitor.visitDownload()
19 | }
20 | },
21 | Upload {
22 | override fun toValue(): String {
23 | return "upload"
24 | }
25 |
26 | override fun visit(visitor: Visitor): R {
27 | return visitor.visitUpload()
28 | }
29 | };
30 |
31 | @JsonValue
32 | abstract fun toValue(): String
33 | abstract fun visit(visitor: Visitor): R
34 | interface Visitor {
35 | fun visitDownload(): R
36 | fun visitUpload(): R
37 | }
38 |
39 | companion object {
40 | @JsonCreator
41 | fun forValue(value: String): Operation? {
42 | for (item in values()) {
43 | if (item.toValue() == value) {
44 | return item
45 | }
46 | }
47 | return null
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Ref.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class Ref(
6 | /**
7 | * Fully-qualified server refspec.
8 | */
9 | @field:JsonProperty(value = "name", required = true)
10 | @param:JsonProperty(value = "name", required = true)
11 | val name: String) {
12 | override fun toString(): String {
13 | return name
14 | }
15 |
16 | companion object {
17 | fun create(ref: String?): Ref? {
18 | return if (ref == null) null else Ref(ref)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/User.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class User(
6 | @field:JsonProperty(value = "name", required = true)
7 | @param:JsonProperty(value = "name", required = true)
8 | val name: String
9 | )
10 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksReq.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class VerifyLocksReq(
6 | /**
7 | * Optional cursor to allow pagination.
8 | */
9 | @field:JsonProperty(value = "cursor") @param:JsonProperty(value = "cursor") val cursor: String?,
10 | /**
11 | * Optional object describing the server ref that the locks belong to.
12 | */
13 | @field:JsonProperty(value = "ref") @param:JsonProperty(value = "ref") val ref: Ref?,
14 | /**
15 | * Optional limit to how many locks to return.
16 | */
17 | @field:JsonProperty(value = "limit") @param:JsonProperty(value = "limit") val limit: Int?)
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksRes.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 |
5 | class VerifyLocksRes(
6 | @field:JsonProperty(value = "ours", required = true)
7 | @param:JsonProperty(value = "ours", required = true)
8 | val ours: List,
9 | @field:JsonProperty(value = "theirs", required = true)
10 | @param:JsonProperty(value = "theirs", required = true)
11 | val theirs: List,
12 | @field:JsonProperty(value = "next_cursor")
13 | @param:JsonProperty(value = "next_cursor")
14 | val nextCursor: String?
15 | )
16 |
--------------------------------------------------------------------------------
/gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/io/InputStreamValidator.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.io
2 |
3 | import ru.bozaro.gitlfs.common.data.Meta
4 | import java.io.IOException
5 | import java.io.InputStream
6 | import java.security.MessageDigest
7 | import java.security.NoSuchAlgorithmException
8 |
9 | /**
10 | * Wrapper for validating hash and size of uploading object.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class InputStreamValidator(stream: InputStream, meta: Meta) : InputStream() {
15 | private var digest: MessageDigest? = null
16 |
17 | private val stream: InputStream
18 |
19 | private val meta: Meta
20 | private var eof: Boolean
21 | private var totalSize: Long
22 |
23 | @Throws(IOException::class)
24 | override fun read(): Int {
25 | if (eof) {
26 | return -1
27 | }
28 | val data = stream.read()
29 | if (data >= 0) {
30 | digest!!.update(data.toByte())
31 | checkSize(1)
32 | } else {
33 | checkSize(-1)
34 | }
35 | return data
36 | }
37 |
38 | @Throws(IOException::class)
39 | private fun checkSize(size: Int) {
40 | if (size > 0) {
41 | totalSize += size.toLong()
42 | }
43 | if (meta.size in 1 until totalSize) {
44 | throw IOException("Input stream too big")
45 | }
46 | if (size < 0) {
47 | eof = true
48 | if (meta.size >= 0 && totalSize != meta.size) {
49 | throw IOException("Unexpected end of stream")
50 | }
51 | val hash = toHexString(digest!!.digest())
52 | if (meta.oid != hash) {
53 | throw IOException("Invalid stream hash")
54 | }
55 | }
56 | }
57 |
58 | @Throws(IOException::class)
59 | override fun read(buffer: ByteArray, off: Int, len: Int): Int {
60 | if (eof) {
61 | return -1
62 | }
63 | val size = stream.read(buffer, off, len)
64 | if (size > 0) {
65 | digest!!.update(buffer, off, size)
66 | }
67 | checkSize(size)
68 | return size
69 | }
70 |
71 | @Throws(IOException::class)
72 | override fun close() {
73 | stream.close()
74 | }
75 |
76 | companion object {
77 | private val hexDigits = "0123456789abcdef".toCharArray()
78 | private fun toHexString(bytes: ByteArray): String {
79 | val sb = StringBuilder(2 * bytes.size)
80 | for (b in bytes) {
81 | sb.append(hexDigits[b.toInt() shr 4 and 0xf]).append(hexDigits[b.toInt() and 0xf])
82 | }
83 | return sb.toString()
84 | }
85 | }
86 |
87 | init {
88 | try {
89 | digest = MessageDigest.getInstance("SHA-256")
90 | } catch (e: NoSuchAlgorithmException) {
91 | throw IOException(e)
92 | }
93 | this.stream = stream
94 | this.meta = meta
95 | eof = false
96 | totalSize = 0
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/BatchReqTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import java.io.IOException
6 | import java.net.URISyntaxException
7 | import java.text.ParseException
8 |
9 | /**
10 | * Test Meta deserialization.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class BatchReqTest {
15 | @Test
16 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
17 | fun parse01() {
18 | val data: BatchReq = SerializeTester.deserialize("batch-req-01.json", BatchReq::class.java)
19 | Assert.assertNotNull(data)
20 | Assert.assertEquals(data.operation, Operation.Upload)
21 | Assert.assertEquals(1, data.objects.size)
22 | val meta: Meta = data.objects[0]
23 | Assert.assertNotNull(meta)
24 | Assert.assertEquals(meta.oid, "1111111")
25 | Assert.assertEquals(meta.size, 123L)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/BatchResTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.databind.util.StdDateFormat
4 | import com.google.common.collect.ImmutableMap
5 | import org.testng.Assert
6 | import org.testng.annotations.Test
7 | import java.io.IOException
8 | import java.net.URI
9 | import java.net.URISyntaxException
10 | import java.text.ParseException
11 |
12 | /**
13 | * Test Meta deserialization.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class BatchResTest {
18 | @Test
19 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
20 | fun parse01() {
21 | val data: BatchRes = SerializeTester.deserialize("batch-res-01.json", BatchRes::class.java)
22 | Assert.assertNotNull(data)
23 | Assert.assertEquals(2, data.objects.size)
24 | run {
25 | val item: BatchItem = data.objects[0]
26 | Assert.assertNotNull(item)
27 | Assert.assertEquals(item.oid, "1111111")
28 | Assert.assertEquals(item.size, 123L)
29 | Assert.assertNull(item.error)
30 | Assert.assertEquals(1, item.links.size)
31 | val link: Link = item.links[LinkType.Download]!!
32 | Assert.assertNotNull(link)
33 | Assert.assertEquals(link.href, URI("https://some-download.com"))
34 | Assert.assertEquals(
35 | link.header,
36 | ImmutableMap.builder()
37 | .put("Authorization", "Basic ...")
38 | .build()
39 | )
40 | Assert.assertEquals(link.expiresAt, StdDateFormat.instance.parse("2015-07-27T21:15:01.000+00:00"))
41 | }
42 | run {
43 | val item: BatchItem = data.objects[1]
44 | Assert.assertNotNull(item)
45 | Assert.assertEquals(item.oid, "2222222")
46 | Assert.assertEquals(item.size, 234L)
47 | Assert.assertTrue(item.links.isEmpty())
48 | val error = item.error!!
49 | Assert.assertNotNull(error)
50 | Assert.assertEquals(error.code, 404)
51 | Assert.assertEquals(error.message, "Object does not exist on the server")
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/CreateLockReqTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import java.io.IOException
6 |
7 | class CreateLockReqTest {
8 | @Test
9 | @Throws(IOException::class)
10 | fun parse01() {
11 | val data: CreateLockReq = SerializeTester.deserialize("create-lock-req-01.json", CreateLockReq::class.java)
12 | Assert.assertNotNull(data)
13 | Assert.assertEquals(data.path, "foo/bar.zip")
14 | Assert.assertNotNull(data.ref)
15 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/CreateLockResTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.databind.util.StdDateFormat
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import java.io.IOException
7 | import java.text.ParseException
8 |
9 | class CreateLockResTest {
10 | @Test
11 | @Throws(IOException::class, ParseException::class)
12 | fun parse01() {
13 | val data: CreateLockRes = SerializeTester.deserialize("create-lock-res-01.json", CreateLockRes::class.java)
14 | Assert.assertNotNull(data)
15 | val lock = data.lock
16 | Assert.assertNotNull(lock)
17 | Assert.assertEquals(lock.id, "some-uuid")
18 | Assert.assertEquals(lock.path, "/path/to/file")
19 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00"))
20 | Assert.assertNotNull(lock.owner)
21 | Assert.assertEquals(lock.owner!!.name, "Jane Doe")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/DateTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import ru.bozaro.gitlfs.common.JsonHelper
6 | import java.text.ParseException
7 |
8 | class DateTest {
9 | @Test
10 | @Throws(ParseException::class)
11 | fun format() {
12 | val str = "2006-01-02T15:04:05.123+00:00"
13 | Assert.assertEquals(JsonHelper.dateFormat.format(JsonHelper.dateFormat.parse(str)), str)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockReqTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import java.io.IOException
6 |
7 | class DeleteLockReqTest {
8 | @Test
9 | @Throws(IOException::class)
10 | fun parse01() {
11 | val data: DeleteLockReq = SerializeTester.deserialize("delete-lock-req-01.json", DeleteLockReq::class.java)
12 | Assert.assertNotNull(data)
13 | Assert.assertTrue(data.isForce())
14 | Assert.assertNotNull(data.ref)
15 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/LinkTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.databind.util.StdDateFormat
4 | import com.google.common.collect.ImmutableMap
5 | import org.testng.Assert
6 | import org.testng.annotations.Test
7 | import java.io.IOException
8 | import java.net.URI
9 | import java.net.URISyntaxException
10 | import java.text.ParseException
11 |
12 | /**
13 | * Test Link deserialization.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class LinkTest {
18 | @Test
19 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
20 | fun parse01() {
21 | val link: Link = SerializeTester.deserialize("link-01.json", Link::class.java)
22 | Assert.assertNotNull(link)
23 | Assert.assertEquals(link.href, URI("https://storage-server.com/OID"))
24 | Assert.assertEquals(link.header,
25 | ImmutableMap.builder()
26 | .put("Authorization", "Basic ...")
27 | .build()
28 | )
29 | }
30 |
31 | @Test
32 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
33 | fun parse02() {
34 | val link: Link = SerializeTester.deserialize("link-02.json", Link::class.java)
35 | Assert.assertNotNull(link)
36 | Assert.assertEquals(link.href, URI("https://api.github.com/lfs/bozaro/git-lfs-java"))
37 | Assert.assertEquals(link.header,
38 | ImmutableMap.builder()
39 | .put("Authorization", "RemoteAuth secret")
40 | .build()
41 | )
42 | Assert.assertEquals(link.expiresAt, StdDateFormat.instance.parse("2015-09-17T19:17:31.000+00:00"))
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/LocksResTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.databind.util.StdDateFormat
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import java.io.IOException
7 | import java.text.ParseException
8 |
9 | class LocksResTest {
10 | @Test
11 | @Throws(IOException::class, ParseException::class)
12 | fun parse01() {
13 | val data: LocksRes = SerializeTester.deserialize("locks-res-01.json", LocksRes::class.java)
14 | Assert.assertNotNull(data)
15 | Assert.assertEquals(data.nextCursor, "optional next ID")
16 | Assert.assertEquals(data.locks.size, 1)
17 | val lock: Lock = data.locks[0]
18 | Assert.assertNotNull(lock)
19 | Assert.assertEquals(lock.id, "some-uuid")
20 | Assert.assertEquals(lock.path, "/path/to/file")
21 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00"))
22 | Assert.assertNotNull(lock.owner)
23 | Assert.assertEquals(lock.owner!!.name, "Jane Doe")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/MetaTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import java.io.IOException
6 | import java.net.URISyntaxException
7 | import java.text.ParseException
8 |
9 | /**
10 | * Test Meta deserialization.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | class MetaTest {
15 | @Test
16 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
17 | fun parse01() {
18 | val meta: Meta = SerializeTester.deserialize("meta-01.json", Meta::class.java)
19 | Assert.assertNotNull(meta)
20 | Assert.assertEquals(meta.oid, "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b")
21 | Assert.assertEquals(meta.size, 130L)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/ObjectResTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.google.common.collect.ImmutableMap
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import java.io.IOException
7 | import java.net.URI
8 | import java.net.URISyntaxException
9 | import java.text.ParseException
10 |
11 | /**
12 | * Test Meta deserialization.
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | class ObjectResTest {
17 | @Test
18 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
19 | fun parse01() {
20 | val res: ObjectRes = SerializeTester.deserialize("object-res-01.json", ObjectRes::class.java)
21 | Assert.assertNotNull(res)
22 | val meta = res.meta!!
23 | Assert.assertNotNull(meta)
24 | Assert.assertEquals(meta.oid, "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b")
25 | Assert.assertEquals(meta.size, 130L)
26 | Assert.assertEquals(2, res.links.size)
27 | val self: Link = res.links[LinkType.Self]!!
28 | Assert.assertNotNull(self)
29 | Assert.assertEquals(self.href, URI("https://storage-server.com/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"))
30 | Assert.assertTrue(self.header.isEmpty())
31 | val link: Link = res.links[LinkType.Upload]!!
32 | Assert.assertNotNull(link)
33 | Assert.assertEquals(link.href, URI("https://storage-server.com/OID"))
34 | Assert.assertEquals(link.header,
35 | ImmutableMap.builder()
36 | .put("Authorization", "Basic ...")
37 | .build()
38 | )
39 | }
40 |
41 | @Test
42 | @Throws(IOException::class, ParseException::class, URISyntaxException::class)
43 | fun parse02() {
44 | val res: ObjectRes = SerializeTester.deserialize("object-res-02.json", ObjectRes::class.java)
45 | Assert.assertNotNull(res)
46 | Assert.assertNull(res.meta)
47 | Assert.assertEquals(1, res.links.size)
48 | val link: Link = res.links[LinkType.Upload]!!
49 | Assert.assertNotNull(link)
50 | Assert.assertEquals(link.href, URI("https://some-upload.com"))
51 | Assert.assertEquals(link.header,
52 | ImmutableMap.builder()
53 | .put("Authorization", "Basic ...")
54 | .build()
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/SerializeTester.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import ru.bozaro.gitlfs.common.JsonHelper
5 |
6 | /**
7 | * Class for searialization tester.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | object SerializeTester {
12 | fun deserialize(path: String?, type: Class?): T {
13 | SerializeTester::class.java.getResourceAsStream(path).use { stream ->
14 | Assert.assertNotNull(stream)
15 | val value: T = JsonHelper.mapper.readValue(stream, type)
16 | Assert.assertNotNull(value)
17 | val json: String = JsonHelper.mapper.writeValueAsString(value)
18 | return JsonHelper.mapper.readValue(json, type)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksReqTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import java.io.IOException
6 |
7 | class VerifyLocksReqTest {
8 | @Test
9 | @Throws(IOException::class)
10 | fun parse01() {
11 | val data: VerifyLocksReq = SerializeTester.deserialize("verify-locks-req-01.json", VerifyLocksReq::class.java)
12 | Assert.assertNotNull(data)
13 | Assert.assertEquals(data.cursor, "optional cursor")
14 | Assert.assertEquals(100, data.limit)
15 | Assert.assertNotNull(data.ref)
16 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksResTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.common.data
2 |
3 | import com.fasterxml.jackson.databind.util.StdDateFormat
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import java.io.IOException
7 | import java.text.ParseException
8 |
9 | class VerifyLocksResTest {
10 | @Test
11 | @Throws(IOException::class, ParseException::class)
12 | fun parse01() {
13 | val data: VerifyLocksRes = SerializeTester.deserialize("verify-locks-res-01.json", VerifyLocksRes::class.java)
14 | Assert.assertNotNull(data)
15 | Assert.assertEquals(data.nextCursor, "optional next ID")
16 | Assert.assertEquals(data.ours.size, 1)
17 | val lock = data.ours[0]
18 | Assert.assertEquals(lock.id, "some-uuid")
19 | Assert.assertEquals(lock.path, "/path/to/file")
20 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00"))
21 | Assert.assertNotNull(lock.owner)
22 | Assert.assertEquals(lock.owner!!.name, "Jane Doe")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/batch-req-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "operation": "upload",
3 | "objects": [
4 | {
5 | "oid": "1111111",
6 | "size": 123
7 | }
8 | ]
9 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/batch-res-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "objects": [
3 | {
4 | "oid": "1111111",
5 | "size": 123,
6 | "actions": {
7 | "download": {
8 | "href": "https://some-download.com",
9 | "header": {
10 | "Authorization": "Basic ..."
11 | },
12 | "expires_at": "2015-07-27T21:15:01Z"
13 | }
14 | }
15 | },
16 | {
17 | "oid": "2222222",
18 | "size": 234,
19 | "error": {
20 | "code": 404,
21 | "message": "Object does not exist on the server"
22 | }
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/create-lock-req-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "path": "foo/bar.zip",
3 | "ref": {
4 | "name": "refs/heads/my-feature"
5 | }
6 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/create-lock-res-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "lock": {
3 | "id": "some-uuid",
4 | "path": "/path/to/file",
5 | "locked_at": "2016-05-17T15:49:06+00:00",
6 | "owner": {
7 | "name": "Jane Doe"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/delete-lock-req-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "force": true,
3 | "ref": {
4 | "name": "refs/heads/my-feature"
5 | }
6 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/link-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "href": "https://storage-server.com/OID",
3 | "header": {
4 | "Authorization": "Basic ..."
5 | }
6 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/link-02.json:
--------------------------------------------------------------------------------
1 | {
2 | "href": "https://api.github.com/lfs/bozaro/git-lfs-java",
3 | "header": {
4 | "Authorization": "RemoteAuth secret"
5 | },
6 | "expires_at": "2015-09-17T19:17:31Z"
7 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/locks-res-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "locks": [
3 | {
4 | "id": "some-uuid",
5 | "path": "/path/to/file",
6 | "locked_at": "2016-05-17T15:49:06+00:00",
7 | "owner": {
8 | "name": "Jane Doe"
9 | }
10 | }
11 | ],
12 | "next_cursor": "optional next ID"
13 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/meta-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "oid": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
3 | "size": 130
4 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/object-res-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "oid": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
3 | "size": 130,
4 | "_links": {
5 | "self": {
6 | "href": "https://storage-server.com/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"
7 | },
8 | "upload": {
9 | "href": "https://storage-server.com/OID",
10 | "header": {
11 | "Authorization": "Basic ..."
12 | }
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/object-res-02.json:
--------------------------------------------------------------------------------
1 | {
2 | "_links": {
3 | "upload": {
4 | "href": "https://some-upload.com",
5 | "header": {
6 | "Authorization": "Basic ..."
7 | }
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/verify-locks-req-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "cursor": "optional cursor",
3 | "limit": 100,
4 | "ref": {
5 | "name": "refs/heads/my-feature"
6 | }
7 | }
--------------------------------------------------------------------------------
/gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/verify-locks-res-01.json:
--------------------------------------------------------------------------------
1 | {
2 | "ours": [
3 | {
4 | "id": "some-uuid",
5 | "path": "/path/to/file",
6 | "locked_at": "2016-05-17T15:49:06+00:00",
7 | "owner": {
8 | "name": "Jane Doe"
9 | }
10 | }
11 | ],
12 | "theirs": [],
13 | "next_cursor": "optional next ID"
14 | }
--------------------------------------------------------------------------------
/gitlfs-pointer/build.gradle.kts:
--------------------------------------------------------------------------------
1 | description = "Java Git-LFS pointer manipulation"
2 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/main/kotlin/ru/bozaro/gitlfs/pointer/Constants.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.pointer
2 |
3 | /**
4 | * Git-lfs constants.
5 | *
6 | * @author Artem V. Navrotskiy
7 | */
8 | object Constants {
9 | const val POINTER_MAX_SIZE = 1024
10 |
11 | const val VERSION_URL = "https://git-lfs.github.com/spec/v1"
12 |
13 | const val OID = "oid"
14 |
15 | const val SIZE = "size"
16 |
17 | const val VERSION = "version"
18 | }
19 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/main/kotlin/ru/bozaro/gitlfs/pointer/Pointer.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.pointer
2 |
3 | import java.io.IOException
4 | import java.io.InputStream
5 | import java.nio.ByteBuffer
6 | import java.nio.charset.StandardCharsets
7 | import java.util.*
8 | import java.util.regex.Pattern
9 |
10 | /**
11 | * Class for read/writer pointer blobs.
12 | * https://github.com/github/git-lfs/blob/master/docs/spec.md
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | object Pointer {
17 | private val PREFIX = (Constants.VERSION + ' ').toByteArray(StandardCharsets.UTF_8)
18 |
19 | private val REQUIRED = arrayOf(
20 | RequiredKey(Constants.OID, Pattern.compile("^[0-9a-z]+:[0-9a-f]+$")),
21 | RequiredKey(Constants.SIZE, Pattern.compile("^\\d+$"))
22 | )
23 |
24 | /**
25 | * Serialize pointer map.
26 | *
27 | * @param pointer Pointer ru.bozaro.gitlfs.common.data.
28 | * @return Pointer content bytes.
29 | */
30 | fun serializePointer(pointer: Map): ByteArray {
31 | val data: MutableMap = TreeMap(pointer)
32 | val buffer = StringBuilder()
33 | // Write version.
34 | run {
35 | var version = data.remove(Constants.VERSION)
36 | if (version == null) {
37 | version = Constants.VERSION_URL
38 | }
39 | buffer.append(Constants.VERSION).append(' ').append(version).append('\n')
40 | }
41 | for ((key, value) in data) {
42 | buffer.append(key).append(' ').append(value).append('\n')
43 | }
44 | return buffer.toString().toByteArray(StandardCharsets.UTF_8)
45 | }
46 |
47 | /**
48 | * Create pointer with oid and size.
49 | *
50 | * @param oid Object oid.
51 | * @param size Object size.
52 | * @return Return pointer ru.bozaro.gitlfs.common.data.
53 | */
54 | fun createPointer(oid: String, size: Long): Map {
55 | val pointer = TreeMap()
56 | pointer[Constants.VERSION] = Constants.VERSION_URL
57 | pointer[Constants.OID] = oid
58 | pointer[Constants.SIZE] = size.toString()
59 | return pointer
60 | }
61 |
62 | /**
63 | * Read pointer ru.bozaro.gitlfs.common.data.
64 | *
65 | * @param stream Input stream.
66 | * @return Return pointer info or null if blob is not a pointer ru.bozaro.gitlfs.common.data.
67 | */
68 | @Throws(IOException::class)
69 | fun parsePointer(stream: InputStream): Map? {
70 | val buffer = ByteArray(Constants.POINTER_MAX_SIZE)
71 | var size = 0
72 | while (size < buffer.size) {
73 | val len = stream.read(buffer, size, buffer.size - size)
74 | if (len <= 0) {
75 | return parsePointer(buffer, 0, size)
76 | }
77 | size += len
78 | }
79 | return null
80 | }
81 |
82 | /**
83 | * Read @{link ru.bozaro.gitlfs.common.data.Pointer}
84 | *
85 | * @return Return pointer info or null if blob is not a {@link ru.bozaro.gitlfs.common.data.Pointer}
86 | */
87 | fun parsePointer(blob: ByteArray, offset: Int = 0, length: Int = blob.size): Map? {
88 | // Check prefix
89 | if (length < PREFIX.size) return null
90 | for (i in PREFIX.indices) {
91 | if (blob[i] != PREFIX[i]) return null
92 | }
93 | // Reading key value pairs
94 | val result = TreeMap()
95 | val decoder = StandardCharsets.UTF_8.newDecoder()
96 | var lastKey: String? = null
97 | var keyOffset = offset
98 | var required = 0
99 | while (keyOffset < length) {
100 | var valueOffset = keyOffset
101 | // Key
102 | while (true) {
103 | valueOffset++
104 | if (valueOffset < length) {
105 | val c = blob[valueOffset]
106 | if (c == ' '.code.toByte()) break
107 | // Keys MUST only use the characters [a-z] [0-9] . -.
108 | if (c >= 'a'.code.toByte() && c <= 'z'.code.toByte()) continue
109 | if (c >= '0'.code.toByte() && c <= '9'.code.toByte()) continue
110 | if (c == '.'.code.toByte() || c == '-'.code.toByte()) continue
111 | }
112 | // Found invalid character.
113 | return null
114 | }
115 | var endOffset = valueOffset
116 | // Value
117 | do {
118 | endOffset++
119 | if (endOffset >= length) return null
120 | // Values MUST NOT contain return or newline characters.
121 | } while (blob[endOffset] != '\n'.code.toByte())
122 | val key = String(blob, keyOffset, valueOffset - keyOffset, StandardCharsets.UTF_8)
123 | val value: String = try {
124 | decoder.decode(ByteBuffer.wrap(blob, valueOffset + 1, endOffset - valueOffset - 1)).toString()
125 | } catch (e: CharacterCodingException) {
126 | return null
127 | }
128 | if (required < REQUIRED.size && REQUIRED[required].name == key) {
129 | if (!REQUIRED[required].pattern.matcher(value).matches()) {
130 | return null
131 | }
132 | required++
133 | }
134 | if (keyOffset > offset) {
135 | if (lastKey != null && key <= lastKey) {
136 | return null
137 | }
138 | lastKey = key
139 | }
140 | if (result.put(key, value) != null) {
141 | return null
142 | }
143 | keyOffset = endOffset + 1
144 | }
145 | // Not found all required fields.
146 | return if (required < REQUIRED.size) {
147 | null
148 | } else result
149 | }
150 |
151 | private class RequiredKey(
152 | val name: String,
153 | val pattern: Pattern
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/kotlin/ru/bozaro/gitlfs/pointer/PointerTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.pointer
2 |
3 | import com.google.common.collect.ImmutableMap
4 | import org.testng.Assert
5 | import org.testng.annotations.DataProvider
6 | import org.testng.annotations.Test
7 | import ru.bozaro.gitlfs.pointer.Pointer.createPointer
8 | import ru.bozaro.gitlfs.pointer.Pointer.parsePointer
9 | import ru.bozaro.gitlfs.pointer.Pointer.serializePointer
10 | import java.io.IOException
11 |
12 | /**
13 | * Pointer parser test.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class PointerTest {
18 | @Test(dataProvider = "parseValidProvider")
19 | @Throws(IOException::class)
20 | fun parseValid(fileName: String, expected: Map) {
21 | javaClass.getResourceAsStream(fileName).use { stream ->
22 | Assert.assertEquals(parsePointer(stream!!), expected)
23 | }
24 | }
25 |
26 | @Test
27 | fun parseAndSerialize() {
28 | val pointer = createPointer("sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393", 12345)
29 | Assert.assertEquals(pointer["oid"], "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
30 | Assert.assertEquals(pointer["size"], "12345")
31 | Assert.assertEquals(pointer.size, 3)
32 | val bytes = serializePointer(pointer)
33 | val parsed: Map? = parsePointer(bytes)
34 | Assert.assertEquals(parsed, pointer)
35 | }
36 |
37 | @Test(dataProvider = "parseInvalidProvider")
38 | @Throws(IOException::class)
39 | fun parseInvalid(fileName: String, description: String) {
40 | javaClass.getResourceAsStream(fileName).use { stream ->
41 | Assert.assertNotNull(stream)
42 | Assert.assertNull(parsePointer(stream!!), description)
43 | }
44 | }
45 |
46 | @DataProvider(name = "parseValidProvider")
47 | fun parseValidProvider(): Array> {
48 | return arrayOf(
49 | arrayOf(
50 | "pointer-valid-01.dat",
51 | ImmutableMap.builder()
52 | .put("version", "https://git-lfs.github.com/spec/v1")
53 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
54 | .put("size", "12345")
55 | .build()
56 | ), arrayOf(
57 | "pointer-valid-02.dat",
58 | ImmutableMap.builder()
59 | .put("version", "https://git-lfs.github.com/spec/v1")
60 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
61 | .put("name", "Текст в UTF-8")
62 | .put("size", "12345")
63 | .build()
64 | ), arrayOf(
65 | "pointer-valid-03.dat",
66 | ImmutableMap.builder()
67 | .put("version", "https://git-lfs.github.com/spec/v1")
68 | .put("object-name", " Foo")
69 | .put("object.id", "F1")
70 | .put("object0123456789", ":)")
71 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
72 | .put("name", "Текст в UTF-8")
73 | .put("size", "12345")
74 | .build()
75 | )
76 | )
77 | }
78 |
79 | @DataProvider(name = "parseInvalidProvider")
80 | fun parseInvalidProvider(): Array> {
81 | return arrayOf(
82 | arrayOf("pointer-invalid-01.dat", "Version is not in first line"),
83 | arrayOf("pointer-invalid-02.dat", "Two empty lines at end of file"),
84 | arrayOf("pointer-invalid-03.dat", "Size not found"),
85 | arrayOf("pointer-invalid-04.dat", "Oid not found"),
86 | arrayOf("pointer-invalid-05.dat", "Version not found"),
87 | arrayOf("pointer-invalid-06.dat", "Invalid items order"),
88 | arrayOf("pointer-invalid-07.dat", "Non utf-8"),
89 | arrayOf("pointer-invalid-08.dat", "Size is not number"),
90 | arrayOf("pointer-invalid-09.dat", "Duplicate line"),
91 | arrayOf("pointer-invalid-10.dat", "Duplicate version")
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-01.dat:
--------------------------------------------------------------------------------
1 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
2 | version https://git-lfs.github.com/spec/v1
3 | size 12345
4 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-02.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
3 | size 12345
4 |
5 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-03.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
3 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-04.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | size 12345
3 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-05.dat:
--------------------------------------------------------------------------------
1 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
2 | size 12345
3 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-06.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | size 12345
3 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
4 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-07.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-as-svn/git-lfs-java/e262811ab6421aaa4a1f31ba24410ce24a2544f4/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-07.dat
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-08.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
3 | size 12o45
4 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-09.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | name 12345
3 | name 12345
4 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
5 | size 12345
6 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-10.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
3 | size 12345
4 | version https://git-lfs.github.com/spec/v1
5 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-01.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
3 | size 12345
4 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-02.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | name Текст в UTF-8
3 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
4 | size 12345
5 |
--------------------------------------------------------------------------------
/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-03.dat:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | name Текст в UTF-8
3 | object-name Foo
4 | object.id F1
5 | object0123456789 :)
6 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
7 | size 12345
8 |
--------------------------------------------------------------------------------
/gitlfs-server/build.gradle.kts:
--------------------------------------------------------------------------------
1 | description = "Java Git-LFS server implementation-free library"
2 |
3 | dependencies {
4 | api(project(":gitlfs-common"))
5 | api("org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2")
6 |
7 | testImplementation(project(":gitlfs-client"))
8 | testImplementation("org.eclipse.jetty:jetty-servlet:11.0.19")
9 | testRuntimeOnly("org.slf4j:slf4j-simple:1.7.36")
10 | }
11 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ContentManager.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import ru.bozaro.gitlfs.common.data.Meta
5 | import java.io.IOException
6 | import java.io.InputStream
7 |
8 | /**
9 | * Interface for store object content.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | interface ContentManager {
14 | /**
15 | * Check access for requested operation and return some user information.
16 | *
17 | * @param request HTTP request.
18 | * @return Object for send object.
19 | */
20 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
21 | fun checkDownloadAccess(request: HttpServletRequest): Downloader
22 |
23 | /**
24 | * Check access for requested operation and return some user information.
25 | *
26 | * @param request HTTP request.
27 | * @return Object for receive object.
28 | */
29 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
30 | fun checkUploadAccess(request: HttpServletRequest): Uploader
31 |
32 | /**
33 | * Get metadata of uploaded object.
34 | *
35 | * @param hash Object metadata (hash and size).
36 | * @return Return metadata of uploaded object.
37 | */
38 | @Throws(IOException::class)
39 | fun getMetadata(hash: String): Meta?
40 |
41 | interface HeaderProvider {
42 | /**
43 | * Generate pointer header information (for example: replace transit Basic auth by Toker auth).
44 | *
45 | * @param header Default header. Can be modified.
46 | * @return Pointer header information.
47 | */
48 | fun createHeader(header: Map): Map {
49 | return header
50 | }
51 | }
52 |
53 | interface Downloader : HeaderProvider {
54 | /**
55 | * Get object from storage.
56 | *
57 | * @param hash Object metadata (hash and size).
58 | * @return Return object stream.
59 | */
60 | @Throws(IOException::class)
61 | fun openObject(hash: String): InputStream
62 |
63 | /**
64 | * Get gzip-compressed object from storage.
65 | *
66 | * @param hash Object metadata (hash and size).
67 | * @return Return gzip-compressed object stream. If gzip-stream is not available return null.
68 | */
69 | @Throws(IOException::class)
70 | fun openObjectGzipped(hash: String): InputStream?
71 | }
72 |
73 | interface Uploader : HeaderProvider {
74 | /**
75 | * Save object to storage.
76 | *
77 | * @param meta Object metadata (hash and size).
78 | * @param content Stream with object ru.bozaro.gitlfs.common.data.
79 | */
80 | @Throws(IOException::class)
81 | fun saveObject(meta: Meta, content: InputStream)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ContentServlet.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.ServletException
4 | import jakarta.servlet.http.HttpServlet
5 | import jakarta.servlet.http.HttpServletRequest
6 | import jakarta.servlet.http.HttpServletResponse
7 | import ru.bozaro.gitlfs.common.Constants
8 | import ru.bozaro.gitlfs.common.JsonHelper
9 | import ru.bozaro.gitlfs.common.data.Meta
10 | import ru.bozaro.gitlfs.common.io.InputStreamValidator
11 | import ru.bozaro.gitlfs.server.internal.ObjectResponse
12 | import ru.bozaro.gitlfs.server.internal.ResponseWriter
13 | import java.io.IOException
14 | import java.util.regex.Pattern
15 |
16 | /**
17 | * Servlet for content storage.
18 | *
19 | * @author Artem V. Navrotskiy
20 | */
21 | class ContentServlet(private val manager: ContentManager) : HttpServlet() {
22 | @Throws(ServletException::class, IOException::class)
23 | override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
24 | try {
25 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) {
26 | processGet(req, req.pathInfo.substring(1)).write(resp)
27 | return
28 | }
29 | } catch (e: ServerError) {
30 | PointerServlet.sendError(resp, e)
31 | return
32 | }
33 | super.doGet(req, resp)
34 | }
35 |
36 | companion object {
37 | val PATTERN_OID: Pattern = Pattern.compile("^/[0-9a-f]{64}$")
38 | }
39 |
40 | @Throws(IOException::class, ServletException::class)
41 | override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
42 | try {
43 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) {
44 | processObjectVerify(req, req.pathInfo.substring(1)).write(resp)
45 | return
46 | }
47 | } catch (e: ServerError) {
48 | PointerServlet.sendError(resp, e)
49 | return
50 | }
51 | super.doPost(req, resp)
52 | }
53 |
54 | @Throws(IOException::class, ServerError::class)
55 | private fun processObjectVerify(req: HttpServletRequest, oid: String): ResponseWriter {
56 | manager.checkUploadAccess(req)
57 | val expectedMeta = JsonHelper.mapper.readValue(req.inputStream, Meta::class.java)
58 | val actualMeta = manager.getMetadata(oid)
59 | if (!expectedMeta.equals(actualMeta)) throw ServerError(
60 | HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
61 | String.format("LFS verification failure: server=%s client=%s", expectedMeta, actualMeta),
62 | null
63 | )
64 | return ResponseWriter { response: HttpServletResponse -> response.status = HttpServletResponse.SC_OK }
65 | }
66 |
67 | @Throws(ServletException::class, IOException::class)
68 | override fun doPut(req: HttpServletRequest, resp: HttpServletResponse) {
69 | try {
70 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) {
71 | processPut(req, req.pathInfo.substring(1)).write(resp)
72 | return
73 | }
74 | } catch (e: ServerError) {
75 | PointerServlet.sendError(resp, e)
76 | return
77 | }
78 | super.doPut(req, resp)
79 | }
80 |
81 | @Throws(ServerError::class, IOException::class)
82 | private fun processPut(req: HttpServletRequest, oid: String): ResponseWriter {
83 | val uploader = manager.checkUploadAccess(req)
84 | val meta = Meta(oid, -1)
85 | uploader.saveObject(meta, InputStreamValidator(req.inputStream, meta))
86 | return ObjectResponse(HttpServletResponse.SC_OK, meta)
87 | }
88 |
89 | @Throws(ServerError::class, IOException::class)
90 | private fun processGet(req: HttpServletRequest, oid: String): ResponseWriter {
91 | val downloader = manager.checkDownloadAccess(req)
92 | val stream = downloader.openObject(oid)
93 | return ResponseWriter { response: HttpServletResponse ->
94 | response.status = HttpServletResponse.SC_OK
95 | response.contentType = Constants.MIME_BINARY
96 | stream.use { stream ->
97 | val buffer = ByteArray(0x10000)
98 | while (true) {
99 | val read = stream.read(buffer)
100 | if (read < 0) break
101 | response.outputStream.write(buffer, 0, read)
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ForbiddenError.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 |
5 | /**
6 | * Forbidden error.
7 | *
8 | * @author Artem V. Navrotskiy
9 | */
10 | class ForbiddenError constructor(message: String = "Access forbidden") :
11 | ServerError(HttpServletResponse.SC_FORBIDDEN, message)
12 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LocalPointerManager.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import ru.bozaro.gitlfs.common.Constants
5 | import ru.bozaro.gitlfs.common.data.*
6 | import ru.bozaro.gitlfs.server.ContentManager.HeaderProvider
7 | import java.io.IOException
8 | import java.net.URI
9 | import java.util.*
10 |
11 | /**
12 | * Pointer manager for local ContentManager.
13 | *
14 | * @author Artem V. Navrotskiy
15 | */
16 | class LocalPointerManager(private val manager: ContentManager, contentLocation: String) : PointerManager {
17 | private val contentLocation: String = if (contentLocation.endsWith("/")) contentLocation else "$contentLocation/"
18 |
19 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
20 | override fun checkUploadAccess(request: HttpServletRequest, selfUrl: URI): PointerManager.Locator {
21 | val headerProvider: HeaderProvider = manager.checkUploadAccess(request)
22 | return createLocator(request, headerProvider, selfUrl)
23 | }
24 |
25 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
26 | override fun checkDownloadAccess(request: HttpServletRequest, selfUrl: URI): PointerManager.Locator {
27 | val headerProvider: HeaderProvider = manager.checkDownloadAccess(request)
28 | return createLocator(request, headerProvider, selfUrl)
29 | }
30 |
31 | private fun createLocator(
32 | request: HttpServletRequest,
33 | headerProvider: HeaderProvider,
34 | selfUrl: URI
35 | ): PointerManager.Locator {
36 | val header = headerProvider.createHeader(createDefaultHeader(request))
37 | return object : PointerManager.Locator {
38 | @Throws(IOException::class)
39 | override fun getLocations(metas: Array): Array {
40 | return metas.map { getLocation(header, selfUrl, it) }.toTypedArray()
41 | }
42 |
43 | @Throws(IOException::class)
44 | fun getLocation(header: Map, selfUrl: URI, meta: Meta): BatchItem {
45 | val storageMeta = manager.getMetadata(meta.oid)
46 | if (storageMeta != null && meta.size >= 0 && storageMeta.size != meta.size) return BatchItem(
47 | meta,
48 | Error(422, "Invalid object size")
49 | )
50 | val links: MutableMap = EnumMap(LinkType::class.java)
51 | val link = Link(selfUrl.resolve(contentLocation).resolve(meta.oid), header, null)
52 | if (storageMeta == null) links[LinkType.Upload] = link else links[LinkType.Download] = link
53 | links[LinkType.Verify] = link
54 | return BatchItem(storageMeta ?: meta, links)
55 | }
56 | }
57 | }
58 |
59 | private fun createDefaultHeader(request: HttpServletRequest): Map {
60 | val auth = request.getHeader(Constants.HEADER_AUTHORIZATION)
61 | val header = HashMap()
62 | if (auth != null) {
63 | header[Constants.HEADER_AUTHORIZATION] = auth
64 | }
65 | return header
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LockManager.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import ru.bozaro.gitlfs.common.LockConflictException
5 | import ru.bozaro.gitlfs.common.VerifyLocksResult
6 | import ru.bozaro.gitlfs.common.data.Lock
7 | import ru.bozaro.gitlfs.common.data.Ref
8 | import java.io.IOException
9 |
10 | interface LockManager {
11 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
12 | fun checkDownloadAccess(request: HttpServletRequest): LockRead
13 |
14 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
15 | fun checkUploadAccess(request: HttpServletRequest): LockWrite
16 | interface LockRead {
17 | @Throws(IOException::class)
18 | fun getLocks(path: String?, lockId: String?, ref: Ref?): List
19 | }
20 |
21 | interface LockWrite : LockRead {
22 | @Throws(LockConflictException::class, IOException::class)
23 | fun lock(path: String, ref: Ref?): Lock
24 |
25 | @Throws(LockConflictException::class, IOException::class)
26 | fun unlock(lockId: String, force: Boolean, ref: Ref?): Lock?
27 |
28 | @Throws(IOException::class)
29 | fun verifyLocks(ref: Ref?): VerifyLocksResult
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LocksServlet.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.ServletException
4 | import jakarta.servlet.http.HttpServlet
5 | import jakarta.servlet.http.HttpServletRequest
6 | import jakarta.servlet.http.HttpServletResponse
7 | import ru.bozaro.gitlfs.common.JsonHelper
8 | import ru.bozaro.gitlfs.common.LockConflictException
9 | import ru.bozaro.gitlfs.common.data.*
10 | import ru.bozaro.gitlfs.server.LockManager.LockRead
11 | import ru.bozaro.gitlfs.server.LockManager.LockWrite
12 | import ru.bozaro.gitlfs.server.internal.ObjectResponse
13 | import ru.bozaro.gitlfs.server.internal.ResponseWriter
14 | import java.io.IOException
15 |
16 | class LocksServlet(private val lockManager: LockManager) : HttpServlet() {
17 | @Throws(ServletException::class, IOException::class)
18 | override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
19 | try {
20 | val lockRead = lockManager.checkDownloadAccess(req)
21 | if (req.pathInfo == null) {
22 | listLocks(req, lockRead).write(resp)
23 | return
24 | }
25 | } catch (e: ServerError) {
26 | PointerServlet.sendError(resp, e)
27 | return
28 | }
29 | super.doGet(req, resp)
30 | }
31 |
32 | @Throws(ServletException::class, IOException::class)
33 | override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
34 | try {
35 | PointerServlet.checkMimeTypes(req)
36 | val lockWrite = lockManager.checkUploadAccess(req)
37 | when {
38 | req.pathInfo == null -> {
39 | createLock(req, lockWrite).write(resp)
40 | return
41 | }
42 | "/verify" == req.pathInfo -> {
43 | verifyLocks(req, lockWrite).write(resp)
44 | return
45 | }
46 | req.pathInfo.endsWith("/unlock") -> {
47 | deleteLock(req, lockWrite, req.pathInfo.substring(1, req.pathInfo.length - 7)).write(resp)
48 | return
49 | }
50 | }
51 | } catch (e: ServerError) {
52 | PointerServlet.sendError(resp, e)
53 | return
54 | }
55 | super.doPost(req, resp)
56 | }
57 |
58 | @Throws(IOException::class)
59 | private fun createLock(req: HttpServletRequest, lockWrite: LockWrite): ResponseWriter {
60 | val createLockReq = JsonHelper.mapper.readValue(req.inputStream, CreateLockReq::class.java)
61 | return try {
62 | val lock = lockWrite.lock(createLockReq.path, createLockReq.ref)
63 | ObjectResponse(HttpServletResponse.SC_CREATED, CreateLockRes(lock))
64 | } catch (e: LockConflictException) {
65 | ObjectResponse(HttpServletResponse.SC_CONFLICT, LockConflictRes(e.message!!, e.lock))
66 | }
67 | }
68 |
69 | @Throws(IOException::class)
70 | private fun verifyLocks(req: HttpServletRequest, lockWrite: LockWrite): ResponseWriter {
71 | val verifyLocksReq = JsonHelper.mapper.readValue(req.inputStream, VerifyLocksReq::class.java)
72 | val result = lockWrite.verifyLocks(verifyLocksReq.ref)
73 | return ObjectResponse(HttpServletResponse.SC_OK, VerifyLocksRes(result.ourLocks, result.theirLocks, null))
74 | }
75 |
76 | @Throws(IOException::class, ServerError::class)
77 | private fun deleteLock(
78 | req: HttpServletRequest,
79 | lockWrite: LockWrite,
80 | lockId: String
81 | ): ResponseWriter {
82 | val deleteLockReq = JsonHelper.mapper.readValue(req.inputStream, DeleteLockReq::class.java)
83 | return try {
84 | val lock = lockWrite.unlock(lockId, deleteLockReq.isForce(), deleteLockReq.ref)
85 | ?: throw ServerError(HttpServletResponse.SC_NOT_FOUND, String.format("Lock %s not found", lockId))
86 | ObjectResponse(HttpServletResponse.SC_OK, CreateLockRes(lock))
87 | } catch (e: LockConflictException) {
88 | ObjectResponse(HttpServletResponse.SC_FORBIDDEN, CreateLockRes(e.lock))
89 | }
90 | }
91 |
92 | @Throws(IOException::class)
93 | private fun listLocks(req: HttpServletRequest, lockRead: LockRead): ResponseWriter {
94 | val refName = req.getParameter("refspec")
95 | val path = req.getParameter("path")
96 | val lockId = req.getParameter("id")
97 | val locks = lockRead.getLocks(path, lockId, Ref.create(refName))
98 | return ObjectResponse(HttpServletResponse.SC_OK, LocksRes(locks, null))
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/PointerManager.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import ru.bozaro.gitlfs.common.data.BatchItem
5 | import ru.bozaro.gitlfs.common.data.Meta
6 | import java.io.IOException
7 | import java.net.URI
8 |
9 | /**
10 | * Interface for lookup pointer information.
11 | *
12 | * @author Artem V. Navrotskiy
13 | */
14 | interface PointerManager {
15 | /**
16 | * Check access for upload objects.
17 | *
18 | * @param request HTTP request.
19 | * @param selfUrl Http URL for this request.
20 | * @return Location provider.
21 | */
22 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
23 | fun checkUploadAccess(request: HttpServletRequest, selfUrl: URI): Locator
24 |
25 | /**
26 | * Check access for download objects.
27 | *
28 | * @param request HTTP request.
29 | * @param selfUrl Http URL for this request.
30 | * @return Location provider.
31 | */
32 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
33 | fun checkDownloadAccess(request: HttpServletRequest, selfUrl: URI): Locator
34 |
35 | fun interface Locator {
36 | /**
37 | * @param metas Object hash array (note: metadata can have negative size for GET object request).
38 | * @return Return batch items with same order and same count as metas array.
39 | */
40 | @Throws(IOException::class)
41 | fun getLocations(metas: Array): Array
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ServerError.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 |
5 | /**
6 | * Server side error exception.
7 | *
8 | * @author Artem V. Navrotskiy
9 | */
10 | open class ServerError : Exception {
11 | val statusCode: Int
12 |
13 | constructor(statusCode: Int, message: String?) : super(message) {
14 | this.statusCode = statusCode
15 | }
16 |
17 | constructor(statusCode: Int, message: String?, cause: Throwable?) : super(message, cause) {
18 | this.statusCode = statusCode
19 | }
20 |
21 | open fun updateHeaders(response: HttpServletResponse) {}
22 | }
23 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/UnauthorizedError.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 |
5 | /**
6 | * Unauthorized error.
7 | *
8 | * @author Artem V. Navrotskiy
9 | */
10 | class UnauthorizedError(private val authenticate: String) :
11 | ServerError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") {
12 | override fun updateHeaders(response: HttpServletResponse) {
13 | super.updateHeaders(response)
14 | response.addHeader("WWW-Authenticate", authenticate)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/internal/ObjectResponse.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server.internal
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON
5 | import ru.bozaro.gitlfs.common.JsonHelper
6 | import java.io.IOException
7 |
8 | /**
9 | * Response with simple JSON output.
10 | *
11 | * @author Artem V. Navrotskiy
12 | */
13 | class ObjectResponse(private val status: Int, private val value: Any) : ResponseWriter {
14 | @Throws(IOException::class)
15 | override fun write(response: HttpServletResponse) {
16 | response.status = status
17 | response.contentType = MIME_LFS_JSON
18 | JsonHelper.mapper.writeValue(response.outputStream, value)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/internal/ResponseWriter.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server.internal
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 | import java.io.IOException
5 |
6 | /**
7 | * HTTP response writer.
8 | *
9 | * @author Artem V. Navrotskiy
10 | */
11 | fun interface ResponseWriter {
12 | @Throws(IOException::class)
13 | fun write(response: HttpServletResponse)
14 | }
15 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/EmbeddedHttpServer.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import jakarta.servlet.Servlet
4 | import org.eclipse.jetty.server.HttpConnectionFactory
5 | import org.eclipse.jetty.server.Server
6 | import org.eclipse.jetty.server.ServerConnector
7 | import org.eclipse.jetty.servlet.ServletHandler
8 | import org.eclipse.jetty.servlet.ServletHolder
9 | import java.net.URI
10 | import java.net.URISyntaxException
11 |
12 | /**
13 | * Embedded HTTP server for servlet testing.
14 | *
15 | * @author Artem V. Navrotskiy
16 | */
17 | class EmbeddedHttpServer : AutoCloseable {
18 | private val server: Server = Server()
19 | private val http: ServerConnector = ServerConnector(server, HttpConnectionFactory())
20 | private val servletHandler: ServletHandler
21 |
22 | val base: URI
23 | get() = try {
24 | URI("http", null, http.host, http.localPort, null, null, null)
25 | } catch (e: URISyntaxException) {
26 | throw IllegalStateException(e)
27 | }
28 |
29 | fun addServlet(pathSpec: String, servlet: Servlet) {
30 | servletHandler.addServletWithMapping(ServletHolder(servlet), pathSpec)
31 | }
32 |
33 | @Throws(Exception::class)
34 | override fun close() {
35 | server.stop()
36 | server.join()
37 | }
38 |
39 | init {
40 | http.port = 0
41 | http.host = "127.0.1.1"
42 | server.addConnector(http)
43 | servletHandler = ServletHandler()
44 | server.handler = servletHandler
45 | server.start()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/EmbeddedLfsServer.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import ru.bozaro.gitlfs.client.auth.AuthProvider
4 |
5 | /**
6 | * Embedded LFS server for servlet testing.
7 | *
8 | * @author Artem V. Navrotskiy
9 | */
10 | class EmbeddedLfsServer(val storage: MemoryStorage, lockManager: LockManager?) : AutoCloseable {
11 | private val server: EmbeddedHttpServer = EmbeddedHttpServer()
12 |
13 | val authProvider: AuthProvider
14 | get() = storage.getAuthProvider(server.base.resolve("/foo/bar.git/info/lfs"))
15 |
16 | @Throws(Exception::class)
17 | override fun close() {
18 | server.close()
19 | }
20 |
21 | init {
22 | server.addServlet("/foo/bar.git/info/lfs/objects/*", PointerServlet(storage, "/foo/bar.git/info/lfs/storage/"))
23 | server.addServlet("/foo/bar.git/info/lfs/storage/*", ContentServlet(storage))
24 | if (lockManager != null) server.addServlet("/foo/bar.git/info/lfs/locks/*", LocksServlet(lockManager))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/LocksTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import org.testng.Assert
4 | import org.testng.annotations.Test
5 | import ru.bozaro.gitlfs.client.Client
6 | import ru.bozaro.gitlfs.common.LockConflictException
7 | import ru.bozaro.gitlfs.common.data.Ref
8 |
9 | class LocksTest {
10 | @Test
11 | @Throws(Exception::class)
12 | fun simple() {
13 | val storage = MemoryStorage(-1)
14 | EmbeddedLfsServer(storage, MemoryLockManager(storage)).use { server ->
15 | val auth = server.authProvider
16 | val client = Client(auth)
17 | val ref = Ref.create("ref/heads/master")
18 | val lock = client.lock("qwe", ref)
19 | Assert.assertNotNull(lock)
20 | try {
21 | client.lock("qwe", ref)
22 | Assert.fail()
23 | } catch (e: LockConflictException) {
24 | Assert.assertEquals(lock.id, e.lock.id)
25 | }
26 | run {
27 | val locks = client.listLocks("qwe", null, ref)
28 | Assert.assertEquals(locks.size, 1)
29 | Assert.assertEquals(locks[0].id, lock.id)
30 | }
31 | run {
32 | val locks = client.verifyLocks(ref)
33 | Assert.assertEquals(locks.ourLocks.size, 1)
34 | Assert.assertEquals(locks.ourLocks[0].id, lock.id)
35 | Assert.assertEquals(locks.theirLocks.size, 0)
36 | }
37 | val unlock = client.unlock(lock.id, true, ref)
38 | Assert.assertNotNull(unlock)
39 | Assert.assertEquals(unlock!!.id, lock.id)
40 | Assert.assertNull(client.unlock(lock.id, false, ref))
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/MemoryLockManager.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import com.google.common.base.Strings
4 | import jakarta.servlet.http.HttpServletRequest
5 | import ru.bozaro.gitlfs.common.LockConflictException
6 | import ru.bozaro.gitlfs.common.VerifyLocksResult
7 | import ru.bozaro.gitlfs.common.data.Lock
8 | import ru.bozaro.gitlfs.common.data.Ref
9 | import ru.bozaro.gitlfs.common.data.User
10 | import ru.bozaro.gitlfs.server.LockManager.LockRead
11 | import ru.bozaro.gitlfs.server.LockManager.LockWrite
12 | import java.io.IOException
13 | import java.util.*
14 | import java.util.concurrent.atomic.AtomicInteger
15 | import java.util.stream.Collectors
16 |
17 | class MemoryLockManager(private val contentManager: ContentManager) : LockManager, LockWrite {
18 | private val nextId = AtomicInteger(1)
19 | private val locks = ArrayList()
20 |
21 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
22 | override fun checkDownloadAccess(request: HttpServletRequest): LockRead {
23 | contentManager.checkDownloadAccess(request)
24 | return this
25 | }
26 |
27 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class)
28 | override fun checkUploadAccess(request: HttpServletRequest): LockWrite {
29 | contentManager.checkUploadAccess(request)
30 | return this
31 | }
32 |
33 | override fun getLocks(path: String?, lockId: String?, ref: Ref?): List {
34 | var stream = locks.stream()
35 | if (!Strings.isNullOrEmpty(path)) stream = stream.filter { lock: Lock -> lock.path == path }
36 | if (!Strings.isNullOrEmpty(lockId)) stream = stream.filter { lock: Lock -> lock.id == lockId }
37 | return stream.collect(Collectors.toList())
38 | }
39 |
40 | @Throws(LockConflictException::class)
41 | override fun lock(path: String, ref: Ref?): Lock {
42 | for (lock in locks) if (lock.path == path) throw LockConflictException(lock)
43 | val lock = Lock(nextId.incrementAndGet().toString(), path, Date(), User("Jane Doe"))
44 | locks.add(lock)
45 | return lock
46 | }
47 |
48 | override fun unlock(lockId: String, force: Boolean, ref: Ref?): Lock? {
49 | var lock: Lock? = null
50 | for (l in locks) {
51 | if (l.id == lockId) {
52 | lock = l
53 | break
54 | }
55 | }
56 | if (lock == null) return null
57 | locks.remove(lock)
58 | return lock
59 | }
60 |
61 | override fun verifyLocks(ref: Ref?): VerifyLocksResult {
62 | return VerifyLocksResult(locks, emptyList())
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/MemoryStorage.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import com.google.common.collect.ImmutableMap
4 | import com.google.common.hash.Hashing
5 | import com.google.common.io.ByteStreams
6 | import jakarta.servlet.http.HttpServletRequest
7 | import org.testng.Assert
8 | import ru.bozaro.gitlfs.client.Client.Companion.generateMeta
9 | import ru.bozaro.gitlfs.client.auth.AuthProvider
10 | import ru.bozaro.gitlfs.client.auth.CachedAuthProvider
11 | import ru.bozaro.gitlfs.client.io.StreamProvider
12 | import ru.bozaro.gitlfs.common.Constants
13 | import ru.bozaro.gitlfs.common.data.Link
14 | import ru.bozaro.gitlfs.common.data.Meta
15 | import ru.bozaro.gitlfs.common.data.Operation
16 | import ru.bozaro.gitlfs.server.ContentManager.Downloader
17 | import ru.bozaro.gitlfs.server.ContentManager.Uploader
18 | import java.io.ByteArrayInputStream
19 | import java.io.FileNotFoundException
20 | import java.io.IOException
21 | import java.io.InputStream
22 | import java.net.URI
23 | import java.util.concurrent.ConcurrentHashMap
24 | import java.util.concurrent.atomic.AtomicInteger
25 |
26 | /**
27 | * Simple in-memory storage.
28 | *
29 | * @author Artem V. Navrotskiy
30 | */
31 | class MemoryStorage(private val tokenMaxUsage: Int) : ContentManager {
32 | private val storage: MutableMap = ConcurrentHashMap()
33 | private val tokenId = AtomicInteger(0)
34 |
35 | @Throws(UnauthorizedError::class)
36 | override fun checkDownloadAccess(request: HttpServletRequest): Downloader {
37 | if (token != request.getHeader(Constants.HEADER_AUTHORIZATION)) {
38 | throw UnauthorizedError("Basic realm=\"Test\"")
39 | }
40 | return object : Downloader {
41 | @Throws(IOException::class)
42 | override fun openObject(hash: String): InputStream {
43 | val data = storage[hash] ?: throw FileNotFoundException()
44 | return ByteArrayInputStream(data)
45 | }
46 |
47 | override fun openObjectGzipped(hash: String): InputStream? {
48 | return null
49 | }
50 | }
51 | }
52 |
53 | private val token: String
54 | get() = if (tokenMaxUsage > 0) {
55 | val token = tokenId.incrementAndGet()
56 | "Bearer Token-" + token / tokenMaxUsage
57 | } else {
58 | "Bearer Token-" + tokenId.get()
59 | }
60 |
61 | @Throws(UnauthorizedError::class)
62 | override fun checkUploadAccess(request: HttpServletRequest): Uploader {
63 | if (token != request.getHeader(Constants.HEADER_AUTHORIZATION)) {
64 | throw UnauthorizedError("Basic realm=\"Test\"")
65 | }
66 | val storage = this
67 | return object : Uploader {
68 | override fun saveObject(meta: Meta, content: InputStream) {
69 | storage.saveObject(meta, content)
70 | }
71 | }
72 | }
73 |
74 | override fun getMetadata(hash: String): Meta? {
75 | val data = storage[hash]
76 | return if (data == null) null else Meta(hash, data.size.toLong())
77 | }
78 |
79 | @Throws(IOException::class)
80 | fun saveObject(meta: Meta, content: InputStream) {
81 | val data = ByteStreams.toByteArray(content)
82 | if (meta.size >= 0) {
83 | Assert.assertEquals(meta.size, data.size.toLong())
84 | }
85 | Assert.assertEquals(meta.oid, Hashing.sha256().hashBytes(data).toString())
86 | storage[meta.oid] = data
87 | }
88 |
89 | @Throws(IOException::class)
90 | fun saveObject(provider: StreamProvider) {
91 | val meta = generateMeta(provider)
92 | provider.stream.use { stream -> saveObject(meta, stream) }
93 | }
94 |
95 | fun getObject(oid: String): ByteArray? {
96 | return storage[oid]
97 | }
98 |
99 | fun getAuthProvider(href: URI): AuthProvider {
100 | return object : CachedAuthProvider() {
101 | override fun getAuthUncached(operation: Operation): Link {
102 | return Link(href, ImmutableMap.of(Constants.HEADER_AUTHORIZATION, token), null)
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/ServerTest.kt:
--------------------------------------------------------------------------------
1 | package ru.bozaro.gitlfs.server
2 |
3 | import com.google.common.io.ByteStreams
4 | import org.testng.Assert
5 | import org.testng.annotations.Test
6 | import ru.bozaro.gitlfs.client.Client
7 | import ru.bozaro.gitlfs.client.Client.Companion.generateMeta
8 | import ru.bozaro.gitlfs.client.io.StringStreamProvider
9 | import ru.bozaro.gitlfs.common.data.BatchReq
10 | import ru.bozaro.gitlfs.common.data.Operation
11 | import java.io.FileNotFoundException
12 | import java.io.InputStream
13 |
14 | /**
15 | * Git LFS server implementation test.
16 | *
17 | * @author Artem V. Navrotskiy
18 | */
19 | class ServerTest {
20 | @Test
21 | @Throws(Exception::class)
22 | fun simpleTest() {
23 | EmbeddedLfsServer(MemoryStorage(-1), null).use { server ->
24 | val auth = server.authProvider
25 | val client = Client(auth)
26 | val streamProvider = StringStreamProvider("Hello, world")
27 | val meta = generateMeta(streamProvider)
28 | // Not uploaded yet.
29 | try {
30 | client.getObject(meta.oid) { `in`: InputStream -> ByteStreams.toByteArray(`in`) }
31 | Assert.fail()
32 | } catch (ignored: FileNotFoundException) {
33 | }
34 | client.postBatch(BatchReq(Operation.Download, listOf(meta)))
35 | // Can upload.
36 | Assert.assertTrue(client.putObject(streamProvider, meta))
37 | // Can download uploaded.
38 | val content = client.getObject(meta.oid) { `in`: InputStream -> ByteStreams.toByteArray(`in`) }
39 | Assert.assertEquals(content, ByteStreams.toByteArray(streamProvider.stream))
40 | // Already uploaded.
41 | Assert.assertFalse(client.putObject(streamProvider, meta))
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-as-svn/git-lfs-java/e262811ab6421aaa4a1f31ba24410ce24a2544f4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | file(".").listFiles().forEach {
2 | if (File(it, "build.gradle.kts").exists())
3 | include(it.name)
4 | }
5 |
--------------------------------------------------------------------------------