From bd0e83e350fc703bcae72a28c41b09d9a9cec594 Mon Sep 17 00:00:00 2001 From: Paul Martin Date: Sat, 10 Oct 2015 12:46:51 +0100 Subject: Git-LFS support + Metadata maintained in append-only JSON file providing complete audit history. + Filestore menu item + Lists filestore items + Current size and availability + Link to GitBlit Filestore help page (top right) + Hooks into existing repository permissions + Uses default repository path for out-of-box operation with Git-LFS client + accessRestrictionFilter now has access to http method and auth header + Testing for servlet and manager --- src/main/java/com/gitblit/Constants.java | 4 + src/main/java/com/gitblit/FederationClient.java | 2 +- src/main/java/com/gitblit/GitBlit.java | 7 +- src/main/java/com/gitblit/guice/CoreModule.java | 3 + src/main/java/com/gitblit/guice/WebModule.java | 6 +- .../java/com/gitblit/manager/FilestoreManager.java | 439 ++++++++++++++++++ .../java/com/gitblit/manager/GitblitManager.java | 73 ++- .../com/gitblit/manager/IFilestoreManager.java | 54 +++ src/main/java/com/gitblit/manager/IGitblit.java | 3 +- .../java/com/gitblit/models/FilestoreModel.java | 159 +++++++ .../gitblit/servlet/AccessRestrictionFilter.java | 54 ++- .../com/gitblit/servlet/DownloadZipFilter.java | 8 +- .../java/com/gitblit/servlet/FilestoreServlet.java | 493 +++++++++++++++++++++ src/main/java/com/gitblit/servlet/GitFilter.java | 78 +++- .../java/com/gitblit/servlet/GitblitContext.java | 2 + src/main/java/com/gitblit/servlet/RawFilter.java | 8 +- src/main/java/com/gitblit/utils/JsonUtils.java | 21 +- src/main/java/com/gitblit/wicket/FilestoreUI.java | 62 +++ .../java/com/gitblit/wicket/GitBlitWebApp.java | 16 +- .../com/gitblit/wicket/GitBlitWebApp.properties | 8 +- .../java/com/gitblit/wicket/GitblitWicketApp.java | 3 + .../com/gitblit/wicket/pages/FilestorePage.html | 37 ++ .../com/gitblit/wicket/pages/FilestorePage.java | 114 +++++ .../com/gitblit/wicket/pages/FilestoreUsage.html | 69 +++ .../com/gitblit/wicket/pages/FilestoreUsage.java | 25 ++ .../java/com/gitblit/wicket/pages/RootPage.java | 1 + 26 files changed, 1714 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/gitblit/manager/FilestoreManager.java create mode 100644 src/main/java/com/gitblit/manager/IFilestoreManager.java create mode 100644 src/main/java/com/gitblit/models/FilestoreModel.java create mode 100644 src/main/java/com/gitblit/servlet/FilestoreServlet.java create mode 100644 src/main/java/com/gitblit/wicket/FilestoreUI.java create mode 100644 src/main/java/com/gitblit/wicket/pages/FilestorePage.html create mode 100644 src/main/java/com/gitblit/wicket/pages/FilestorePage.java create mode 100644 src/main/java/com/gitblit/wicket/pages/FilestoreUsage.html create mode 100644 src/main/java/com/gitblit/wicket/pages/FilestoreUsage.java (limited to 'src/main/java/com/gitblit') diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java index 787d7267..4aa8c0ca 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -60,6 +60,8 @@ public class Constants { public static final String R_PATH = "/r/"; public static final String GIT_PATH = "/git/"; + + public static final String REGEX_SHA256 = "[a-fA-F0-9]{64}"; public static final String ZIP_PATH = "/zip/"; @@ -140,6 +142,8 @@ public class Constants { public static final String ATTRIB_AUTHTYPE = NAME + ":authentication-type"; public static final String ATTRIB_AUTHUSER = NAME + ":authenticated-user"; + + public static final String R_LFS = "info/lfs/"; public static String getVersion() { String v = Constants.class.getPackage().getImplementationVersion(); diff --git a/src/main/java/com/gitblit/FederationClient.java b/src/main/java/com/gitblit/FederationClient.java index 487080e5..64ff0172 100644 --- a/src/main/java/com/gitblit/FederationClient.java +++ b/src/main/java/com/gitblit/FederationClient.java @@ -100,7 +100,7 @@ public class FederationClient { UserManager users = new UserManager(runtime, null).start(); RepositoryManager repositories = new RepositoryManager(runtime, null, users).start(); FederationManager federation = new FederationManager(runtime, notifications, repositories).start(); - IGitblit gitblit = new GitblitManager(null, null, runtime, null, notifications, users, null, repositories, null, federation); + IGitblit gitblit = new GitblitManager(null, null, runtime, null, notifications, users, null, repositories, null, federation, null); FederationPullService puller = new FederationPullService(gitblit, federation.getFederationRegistrations()) { @Override diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index 68a91bb5..4e25d5c6 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -18,6 +18,7 @@ package com.gitblit; import com.gitblit.manager.GitblitManager; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; +import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.INotificationManager; import com.gitblit.manager.IPluginManager; import com.gitblit.manager.IProjectManager; @@ -52,7 +53,8 @@ public class GitBlit extends GitblitManager { IAuthenticationManager authenticationManager, IRepositoryManager repositoryManager, IProjectManager projectManager, - IFederationManager federationManager) { + IFederationManager federationManager, + IFilestoreManager filestoreManager) { super( publicKeyManagerProvider, @@ -64,6 +66,7 @@ public class GitBlit extends GitblitManager { authenticationManager, repositoryManager, projectManager, - federationManager); + federationManager, + filestoreManager); } } diff --git a/src/main/java/com/gitblit/guice/CoreModule.java b/src/main/java/com/gitblit/guice/CoreModule.java index a942b2ec..e2d14399 100644 --- a/src/main/java/com/gitblit/guice/CoreModule.java +++ b/src/main/java/com/gitblit/guice/CoreModule.java @@ -20,8 +20,10 @@ import com.gitblit.GitBlit; import com.gitblit.IStoredSettings; import com.gitblit.manager.AuthenticationManager; import com.gitblit.manager.FederationManager; +import com.gitblit.manager.FilestoreManager; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; +import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.IGitblit; import com.gitblit.manager.INotificationManager; import com.gitblit.manager.IPluginManager; @@ -72,6 +74,7 @@ public class CoreModule extends AbstractModule { bind(IRepositoryManager.class).to(RepositoryManager.class); bind(IProjectManager.class).to(ProjectManager.class); bind(IFederationManager.class).to(FederationManager.class); + bind(IFilestoreManager.class).to(FilestoreManager.class); // the monolithic manager bind(IGitblit.class).to(GitBlit.class); diff --git a/src/main/java/com/gitblit/guice/WebModule.java b/src/main/java/com/gitblit/guice/WebModule.java index a4062703..7c83e455 100644 --- a/src/main/java/com/gitblit/guice/WebModule.java +++ b/src/main/java/com/gitblit/guice/WebModule.java @@ -26,6 +26,7 @@ import com.gitblit.servlet.DownloadZipFilter; import com.gitblit.servlet.DownloadZipServlet; import com.gitblit.servlet.EnforceAuthenticationFilter; import com.gitblit.servlet.FederationServlet; +import com.gitblit.servlet.FilestoreServlet; import com.gitblit.servlet.GitFilter; import com.gitblit.servlet.GitServlet; import com.gitblit.servlet.LogoServlet; @@ -62,12 +63,14 @@ public class WebModule extends ServletModule { bind(AvatarGenerator.class).toProvider(AvatarGeneratorProvider.class); // servlets + serveRegex(FilestoreServlet.REGEX_PATH).with(FilestoreServlet.class); serve(fuzzy(Constants.R_PATH), fuzzy(Constants.GIT_PATH)).with(GitServlet.class); serve(fuzzy(Constants.RAW_PATH)).with(RawServlet.class); serve(fuzzy(Constants.PAGES)).with(PagesServlet.class); serve(fuzzy(Constants.RPC_PATH)).with(RpcServlet.class); serve(fuzzy(Constants.ZIP_PATH)).with(DownloadZipServlet.class); serve(fuzzy(Constants.SYNDICATION_PATH)).with(SyndicationServlet.class); + serve(fuzzy(Constants.FEDERATION_PATH)).with(FederationServlet.class); serve(fuzzy(Constants.SPARKLESHARE_INVITE_PATH)).with(SparkleShareInviteServlet.class); @@ -98,7 +101,8 @@ public class WebModule extends ServletModule { filter(fuzzy(Constants.RPC_PATH)).through(RpcFilter.class); filter(fuzzy(Constants.ZIP_PATH)).through(DownloadZipFilter.class); filter(fuzzy(Constants.SYNDICATION_PATH)).through(SyndicationFilter.class); - + + // Wicket String toIgnore = Joiner.on(",").join(Constants.R_PATH, Constants.GIT_PATH, Constants.RAW_PATH, Constants.PAGES, Constants.RPC_PATH, Constants.ZIP_PATH, Constants.SYNDICATION_PATH, diff --git a/src/main/java/com/gitblit/manager/FilestoreManager.java b/src/main/java/com/gitblit/manager/FilestoreManager.java new file mode 100644 index 00000000..33672e4a --- /dev/null +++ b/src/main/java/com/gitblit/manager/FilestoreManager.java @@ -0,0 +1,439 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.manager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.bouncycastle.util.io.StreamOverflowException; +import org.eclipse.jetty.io.EofException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.models.FilestoreModel.Status; +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.JsonUtils.GmtDateTypeAdapter; +import com.google.gson.ExclusionStrategy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * FilestoreManager handles files uploaded via: + * + git-lfs + * + ticket attachment (TBD) + * + * Files are stored using their SHA256 hash (as per git-lfs) + * If the same file is uploaded through different repositories no additional space is used + * Access is controlled through the current repository permissions. + * + * TODO: Identify what and how the actual BLOBs should work with federation + * + * @author Paul Martin + * + */ +@Singleton +public class FilestoreManager implements IFilestoreManager { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final IRuntimeManager runtimeManager; + + private final IStoredSettings settings; + + public static final int UNDEFINED_SIZE = -1; + + private static final String METAFILE = "filestore.json"; + + private static final String METAFILE_TMP = "filestore.json.tmp"; + + protected static final Type METAFILE_TYPE = new TypeToken>() {}.getType(); + + private Map fileCache = new ConcurrentHashMap(); + + + @Inject + FilestoreManager( + IRuntimeManager runtimeManager) { + this.runtimeManager = runtimeManager; + this.settings = runtimeManager.getSettings(); + } + + @Override + public IManager start() { + + //Try to load any existing metadata + File metadata = new File(getStorageFolder(), METAFILE); + + if (metadata.exists()) { + Collection items = null; + + Gson gson = gson(); + try (FileReader file = new FileReader(metadata)) { + items = gson.fromJson(file, METAFILE_TYPE); + file.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + + for(Iterator itr = items.iterator(); itr.hasNext(); ) { + FilestoreModel model = itr.next(); + fileCache.put(model.oid, model); + } + + logger.info("Loaded {} items from filestore metadata file", fileCache.size()); + } + else + { + logger.info("No filestore metadata file found"); + } + + return this; + } + + @Override + public IManager stop() { + return this; + } + + + @Override + public boolean isValidOid(String oid) { + //NOTE: Assuming SHA256 support only as per git-lfs + return Pattern.matches("[a-fA-F0-9]{64}", oid); + } + + @Override + public FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo) { + + //Handle access control + if (!user.canPush(repo)) { + if (user == UserModel.ANONYMOUS) { + return Status.AuthenticationRequired; + } else { + return Status.Error_Unauthorized; + } + } + + //Handle object details + if (!isValidOid(oid)) { return Status.Error_Invalid_Oid; } + + if (fileCache.containsKey(oid)) { + FilestoreModel item = fileCache.get(oid); + + if (!item.isInErrorState() && (size != UNDEFINED_SIZE) && (item.getSize() != size)) { + return Status.Error_Size_Mismatch; + } + + item.addRepository(repo.name); + + if (item.isInErrorState()) { + item.reset(user, size); + } + } else { + + if (size < 0) {return Status.Error_Invalid_Size; } + if ((getMaxUploadSize() != UNDEFINED_SIZE) && (size > getMaxUploadSize())) { return Status.Error_Exceeds_Size_Limit; } + + FilestoreModel model = new FilestoreModel(oid, size, user, repo.name); + fileCache.put(oid, model); + saveFilestoreModel(model); + } + + return fileCache.get(oid).getStatus(); + } + + @Override + public FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn) { + + //Access control and object logic + Status state = addObject(oid, size, user, repo); + + if (state != Status.Upload_Pending) { + return state; + } + + FilestoreModel model = fileCache.get(oid); + + if (!model.actionUpload(user)) { + return Status.Upload_In_Progress; + } else { + long actualSize = 0; + File file = getStoragePath(oid); + + try { + file.getParentFile().mkdirs(); + file.createNewFile(); + + try (FileOutputStream streamOut = new FileOutputStream(file)) { + + actualSize = IOUtils.copyLarge(streamIn, streamOut); + + streamOut.flush(); + streamOut.close(); + + if (model.getSize() != actualSize) { + model.setStatus(Status.Error_Size_Mismatch, user); + + logger.warn(MessageFormat.format("Failed to upload blob {0} due to size mismatch, expected {1} got {2}", + oid, model.getSize(), actualSize)); + } else { + String actualOid = ""; + + try (FileInputStream fileForHash = new FileInputStream(file)) { + actualOid = DigestUtils.sha256Hex(fileForHash); + fileForHash.close(); + } + + if (oid.equalsIgnoreCase(actualOid)) { + model.setStatus(Status.Available, user); + } else { + model.setStatus(Status.Error_Hash_Mismatch, user); + + logger.warn(MessageFormat.format("Failed to upload blob {0} due to hash mismatch, got {1}", oid, actualOid)); + } + } + } + } catch (Exception e) { + + model.setStatus(Status.Error_Unknown, user); + logger.warn(MessageFormat.format("Failed to upload blob {0}", oid), e); + } finally { + saveFilestoreModel(model); + } + + if (model.isInErrorState()) { + file.delete(); + model.removeRepository(repo.name); + } + } + + return model.getStatus(); + } + + private FilestoreModel.Status canGetObject(String oid, UserModel user, RepositoryModel repo) { + + //Access Control + if (!user.canView(repo)) { + if (user == UserModel.ANONYMOUS) { + return Status.AuthenticationRequired; + } else { + return Status.Error_Unauthorized; + } + } + + //Object Logic + if (!isValidOid(oid)) { + return Status.Error_Invalid_Oid; + } + + if (!fileCache.containsKey(oid)) { + return Status.Unavailable; + } + + FilestoreModel item = fileCache.get(oid); + + if (item.getStatus() == Status.Available) { + return Status.Available; + } + + return Status.Unavailable; + } + + @Override + public FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo) { + + if (canGetObject(oid, user, repo) == Status.Available) { + return fileCache.get(oid); + } + + return null; + } + + @Override + public FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut) { + + //Access control and object logic + Status status = canGetObject(oid, user, repo); + + if (status != Status.Available) { + return status; + } + + FilestoreModel item = fileCache.get(oid); + + if (streamOut != null) { + try (FileInputStream streamIn = new FileInputStream(getStoragePath(oid))) { + + IOUtils.copyLarge(streamIn, streamOut); + + streamOut.flush(); + streamIn.close(); + } catch (EofException e) { + logger.error(MessageFormat.format("Client aborted connection for {0}", oid), e); + return Status.Error_Unexpected_Stream_End; + } catch (Exception e) { + logger.error(MessageFormat.format("Failed to download blob {0}", oid), e); + return Status.Error_Unknown; + } + } + + return item.getStatus(); + } + + @Override + public List getAllObjects() { + return new ArrayList(fileCache.values()); + } + + @Override + public File getStorageFolder() { + return runtimeManager.getFileOrFolder(Keys.filestore.storageFolder, "${baseFolder}/lfs"); + } + + @Override + public File getStoragePath(String oid) { + return new File(getStorageFolder(), oid.substring(0, 2).concat("/").concat(oid.substring(2))); + } + + @Override + public long getMaxUploadSize() { + return settings.getLong(Keys.filestore.maxUploadSize, -1); + } + + @Override + public long getFilestoreUsedByteCount() { + Iterator iterator = fileCache.values().iterator(); + long total = 0; + + while (iterator.hasNext()) { + + FilestoreModel item = iterator.next(); + if (item.getStatus() == Status.Available) { + total += item.getSize(); + } + } + + return total; + } + + @Override + public long getFilestoreAvailableByteCount() { + + try { + return Files.getFileStore(getStorageFolder().toPath()).getUsableSpace(); + } catch (IOException e) { + logger.error(MessageFormat.format("Failed to retrive available space in Filestore {0}", e)); + } + + return UNDEFINED_SIZE; + }; + + private synchronized void saveFilestoreModel(FilestoreModel model) { + + File metaFile = new File(getStorageFolder(), METAFILE); + File metaFileTmp = new File(getStorageFolder(), METAFILE_TMP); + boolean isNewFile = false; + + try { + if (!metaFile.exists()) { + metaFile.getParentFile().mkdirs(); + metaFile.createNewFile(); + isNewFile = true; + } + FileUtils.copyFile(metaFile, metaFileTmp); + + } catch (IOException e) { + logger.error("Writing filestore model to file {0}, {1}", METAFILE, e); + } + + try (RandomAccessFile fs = new RandomAccessFile(metaFileTmp, "rw")) { + + if (isNewFile) { + fs.writeBytes("["); + } else { + fs.seek(fs.length() - 1); + fs.writeBytes(","); + } + + fs.writeBytes(gson().toJson(model)); + fs.writeBytes("]"); + + fs.close(); + + } catch (IOException e) { + logger.error("Writing filestore model to file {0}, {1}", METAFILE_TMP, e); + } + + try { + if (metaFileTmp.exists()) { + FileUtils.copyFile(metaFileTmp, metaFile); + + metaFileTmp.delete(); + } else { + logger.error("Writing filestore model to file {0}", METAFILE); + } + } + catch (IOException e) { + logger.error("Writing filestore model to file {0}, {1}", METAFILE, e); + } + } + + /* + * Intended for testing purposes only + */ + public void clearFilestoreCache() { + fileCache.clear(); + } + + private static Gson gson(ExclusionStrategy... strategies) { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter()); + if (!ArrayUtils.isEmpty(strategies)) { + builder.setExclusionStrategies(strategies); + } + return builder.create(); + } + +} diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 8d25b3f5..a34e29d7 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -21,6 +21,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.lang.reflect.Type; import java.text.MessageFormat; import java.util.ArrayList; @@ -58,6 +59,7 @@ import com.gitblit.extensions.RepositoryLifeCycleListener; import com.gitblit.models.FederationModel; import com.gitblit.models.FederationProposal; import com.gitblit.models.FederationSet; +import com.gitblit.models.FilestoreModel; import com.gitblit.models.ForkModel; import com.gitblit.models.GitClientApplication; import com.gitblit.models.Mailing; @@ -132,6 +134,8 @@ public class GitblitManager implements IGitblit { protected final IFederationManager federationManager; + protected final IFilestoreManager filestoreManager; + @Inject public GitblitManager( Provider publicKeyManagerProvider, @@ -143,7 +147,8 @@ public class GitblitManager implements IGitblit { IAuthenticationManager authenticationManager, IRepositoryManager repositoryManager, IProjectManager projectManager, - IFederationManager federationManager) { + IFederationManager federationManager, + IFilestoreManager filestoreManager) { this.publicKeyManagerProvider = publicKeyManagerProvider; this.ticketServiceProvider = ticketServiceProvider; @@ -157,6 +162,7 @@ public class GitblitManager implements IGitblit { this.repositoryManager = repositoryManager; this.projectManager = projectManager; this.federationManager = federationManager; + this.filestoreManager = filestoreManager; } @Override @@ -1238,6 +1244,70 @@ public class GitblitManager implements IGitblit { return repositoryManager.isIdle(repository); } + /* + * FILE STORAGE MANAGER + */ + + @Override + public boolean isValidOid(String oid) { + return filestoreManager.isValidOid(oid); + } + + @Override + public FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo) { + return filestoreManager.addObject(oid, size, user, repo); + } + + @Override + public FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo) { + return filestoreManager.getObject(oid, user, repo); + }; + + @Override + public FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn ) { + return filestoreManager.uploadBlob(oid, size, user, repo, streamIn); + } + + @Override + public FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut ) { + return filestoreManager.downloadBlob(oid, user, repo, streamOut); + } + + @Override + public List getAllObjects() { + return filestoreManager.getAllObjects(); + } + + @Override + public File getStorageFolder() { + return filestoreManager.getStorageFolder(); + } + + @Override + public File getStoragePath(String oid) { + return filestoreManager.getStoragePath(oid); + } + + @Override + public long getMaxUploadSize() { + return filestoreManager.getMaxUploadSize(); + }; + + @Override + public void clearFilestoreCache() { + filestoreManager.clearFilestoreCache(); + }; + + @Override + public long getFilestoreUsedByteCount() { + return filestoreManager.getFilestoreUsedByteCount(); + }; + + @Override + public long getFilestoreAvailableByteCount() { + return filestoreManager.getFilestoreAvailableByteCount(); + }; + /* * PLUGIN MANAGER */ @@ -1341,4 +1411,5 @@ public class GitblitManager implements IGitblit { public PluginRelease lookupRelease(String pluginId, String version) { return pluginManager.lookupRelease(pluginId, version); } + } diff --git a/src/main/java/com/gitblit/manager/IFilestoreManager.java b/src/main/java/com/gitblit/manager/IFilestoreManager.java new file mode 100644 index 00000000..0720650c --- /dev/null +++ b/src/main/java/com/gitblit/manager/IFilestoreManager.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.manager; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; + + +public interface IFilestoreManager extends IManager { + + boolean isValidOid(String oid); + + FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo); + + FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo); + + FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn ); + + FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut ); + + List getAllObjects(); + + File getStorageFolder(); + + File getStoragePath(String oid); + + long getMaxUploadSize(); + + void clearFilestoreCache(); + + long getFilestoreUsedByteCount(); + + long getFilestoreAvailableByteCount(); + +} diff --git a/src/main/java/com/gitblit/manager/IGitblit.java b/src/main/java/com/gitblit/manager/IGitblit.java index 6c5b374c..489de62d 100644 --- a/src/main/java/com/gitblit/manager/IGitblit.java +++ b/src/main/java/com/gitblit/manager/IGitblit.java @@ -33,7 +33,8 @@ public interface IGitblit extends IManager, IAuthenticationManager, IRepositoryManager, IProjectManager, - IFederationManager { + IFederationManager, + IFilestoreManager { /** * Creates a complete user object. diff --git a/src/main/java/com/gitblit/models/FilestoreModel.java b/src/main/java/com/gitblit/models/FilestoreModel.java new file mode 100644 index 00000000..ff7b210e --- /dev/null +++ b/src/main/java/com/gitblit/models/FilestoreModel.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.models; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A FilestoreModel represents a file stored outside a repository but referenced by the repository using a unique objectID + * + * @author Paul Martin + * + */ +public class FilestoreModel implements Serializable { + + private static final long serialVersionUID = 1L; + + public final String oid; + + private Long size; + private Status status; + + //Audit + private String stateChangedBy; + private Date stateChangedOn; + + //Access Control + private List repositories; + + public FilestoreModel(String id, long expectedSize, UserModel user, String repo) { + oid = id; + size = expectedSize; + status = Status.Upload_Pending; + stateChangedBy = user.getName(); + stateChangedOn = new Date(); + repositories = new ArrayList(); + repositories.add(repo); + } + + public synchronized long getSize() { + return size; + } + + public synchronized Status getStatus() { + return status; + } + + public synchronized String getChangedBy() { + return stateChangedBy; + } + + public synchronized Date getChangedOn() { + return stateChangedOn; + } + + public synchronized void setStatus(Status status, UserModel user) { + this.status = status; + stateChangedBy = user.getName(); + stateChangedOn = new Date(); + } + + public synchronized void reset(UserModel user, long size) { + status = Status.Upload_Pending; + stateChangedBy = user.getName(); + stateChangedOn = new Date(); + this.size = size; + } + + /* + * Handles possible race condition with concurrent connections + * @return true if action can proceed, false otherwise + */ + public synchronized boolean actionUpload(UserModel user) { + if (status == Status.Upload_Pending) { + status = Status.Upload_In_Progress; + stateChangedBy = user.getName(); + stateChangedOn = new Date(); + return true; + } + + return false; + } + + public synchronized boolean isInErrorState() { + return (this.status.value < 0); + } + + public synchronized void addRepository(String repo) { + if (!repositories.contains(repo)) { + repositories.add(repo); + } + } + + public synchronized void removeRepository(String repo) { + repositories.remove(repo); + } + + public static enum Status { + + Deleted(-30), + AuthenticationRequired(-20), + + Error_Unknown(-8), + Error_Unexpected_Stream_End(-7), + Error_Invalid_Oid(-6), + Error_Invalid_Size(-5), + Error_Hash_Mismatch(-4), + Error_Size_Mismatch(-3), + Error_Exceeds_Size_Limit(-2), + Error_Unauthorized(-1), + //Negative values provide additional information and may be treated as 0 when not required + Unavailable(0), + Upload_Pending(1), + Upload_In_Progress(2), + Available(3); + + final int value; + + Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public String toString() { + return name().toLowerCase().replace('_', ' '); + } + + public static Status fromState(int state) { + for (Status s : values()) { + if (s.getValue() == state) { + return s; + } + } + throw new NoSuchElementException(String.valueOf(state)); + } + } + +} + diff --git a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java index ee4a91aa..bfbc0899 100644 --- a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java +++ b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java @@ -17,6 +17,8 @@ package com.gitblit.servlet; import java.io.IOException; import java.text.MessageFormat; +import java.util.Collections; +import java.util.Iterator; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -84,16 +86,17 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { * * @return true if the filter allows repository creation */ - protected abstract boolean isCreationAllowed(); + protected abstract boolean isCreationAllowed(String action); /** * Determine if the action may be executed on the repository. * * @param repository * @param action + * @param method * @return true if the action may be performed */ - protected abstract boolean isActionAllowed(RepositoryModel repository, String action); + protected abstract boolean isActionAllowed(RepositoryModel repository, String action, String method); /** * Determine if the repository requires authentication. @@ -102,7 +105,7 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { * @param action * @return true if authentication required */ - protected abstract boolean requiresAuthentication(RepositoryModel repository, String action); + protected abstract boolean requiresAuthentication(RepositoryModel repository, String action, String method); /** * Determine if the user can access the repository and perform the specified @@ -126,7 +129,26 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { protected RepositoryModel createRepository(UserModel user, String repository, String action) { return null; } - + + /** + * Allows authentication header to be altered based on the action requested + * Default is WWW-Authenticate + * @param action + * @return authentication type header + */ + protected String getAuthenticationHeader(String action) { + return "WWW-Authenticate"; + } + + /** + * Allows request headers to be used as part of filtering + * @param request + * @return true (default) if headers are valid, false otherwise + */ + protected boolean hasValidRequestHeader(String action, HttpServletRequest request) { + return true; + } + /** * doFilter does the actual work of preprocessing the request to ensure that * the user may proceed. @@ -163,13 +185,14 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { // Load the repository model RepositoryModel model = repositoryManager.getRepositoryModel(repository); if (model == null) { - if (isCreationAllowed()) { + if (isCreationAllowed(urlRequestType)) { if (user == null) { // challenge client to provide credentials for creation. send 401. if (runtimeManager.isDebugMode()) { logger.info(MessageFormat.format("ARF: CREATE CHALLENGE {0}", fullUrl)); } - httpResponse.setHeader("WWW-Authenticate", CHALLENGE); + + httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } else { @@ -188,7 +211,7 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { } // Confirm that the action may be executed on the repository - if (!isActionAllowed(model, urlRequestType)) { + if (!isActionAllowed(model, urlRequestType, httpRequest.getMethod())) { logger.info(MessageFormat.format("ARF: action {0} on {1} forbidden ({2})", urlRequestType, model, HttpServletResponse.SC_FORBIDDEN)); httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); @@ -210,13 +233,13 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { } // BASIC authentication challenge and response processing - if (!StringUtils.isEmpty(urlRequestType) && requiresAuthentication(model, urlRequestType)) { + if (!StringUtils.isEmpty(urlRequestType) && requiresAuthentication(model, urlRequestType, httpRequest.getMethod())) { if (user == null) { // challenge client to provide credentials. send 401. if (runtimeManager.isDebugMode()) { logger.info(MessageFormat.format("ARF: CHALLENGE {0}", fullUrl)); } - httpResponse.setHeader("WWW-Authenticate", CHALLENGE); + httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } else { @@ -248,4 +271,17 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { // pass processing to the restricted servlet. chain.doFilter(authenticatedRequest, httpResponse); } + + public static boolean hasContentInRequestHeader(HttpServletRequest request, String headerName, String content) + { + Iterator headerItr = Collections.list(request.getHeaders(headerName)).iterator(); + + while (headerItr.hasNext()) { + if (headerItr.next().contains(content)) { + return true; + } + } + + return false; + } } \ No newline at end of file diff --git a/src/main/java/com/gitblit/servlet/DownloadZipFilter.java b/src/main/java/com/gitblit/servlet/DownloadZipFilter.java index de471482..146f6d4f 100644 --- a/src/main/java/com/gitblit/servlet/DownloadZipFilter.java +++ b/src/main/java/com/gitblit/servlet/DownloadZipFilter.java @@ -81,7 +81,7 @@ public class DownloadZipFilter extends AccessRestrictionFilter { * @return true if the filter allows repository creation */ @Override - protected boolean isCreationAllowed() { + protected boolean isCreationAllowed(String action) { return false; } @@ -90,10 +90,11 @@ public class DownloadZipFilter extends AccessRestrictionFilter { * * @param repository * @param action + * @param method * @return true if the action may be performed */ @Override - protected boolean isActionAllowed(RepositoryModel repository, String action) { + protected boolean isActionAllowed(RepositoryModel repository, String action, String method) { return true; } @@ -102,10 +103,11 @@ public class DownloadZipFilter extends AccessRestrictionFilter { * * @param repository * @param action + * @param method * @return true if authentication required */ @Override - protected boolean requiresAuthentication(RepositoryModel repository, String action) { + protected boolean requiresAuthentication(RepositoryModel repository, String action, String method) { return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW); } diff --git a/src/main/java/com/gitblit/servlet/FilestoreServlet.java b/src/main/java/com/gitblit/servlet/FilestoreServlet.java new file mode 100644 index 00000000..19751483 --- /dev/null +++ b/src/main/java/com/gitblit/servlet/FilestoreServlet.java @@ -0,0 +1,493 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants; +import com.gitblit.IStoredSettings; +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.FilestoreModel.Status; +import com.gitblit.manager.FilestoreManager; +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.utils.JsonUtils; + + +/** + * Handles large file storage as per the Git LFS v1 Batch API + * + * Further details can be found at https://github.com/github/git-lfs + * + * @author Paul Martin + */ +@Singleton +public class FilestoreServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + public static final int PROTOCOL_VERSION = 1; + + public static final String GIT_LFS_META_MIME = "application/vnd.git-lfs+json"; + + public static final String REGEX_PATH = "^(.*?)/(r|git)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")"; + public static final int REGEX_GROUP_BASE_URI = 1; + public static final int REGEX_GROUP_PREFIX = 2; + public static final int REGEX_GROUP_REPOSITORY = 3; + public static final int REGEX_GROUP_ENDPOINT = 4; + + protected final Logger logger; + + private static IGitblit gitblit; + + @Inject + public FilestoreServlet(IStoredSettings settings, IGitblit gitblit) { + + super(); + logger = LoggerFactory.getLogger(getClass()); + + FilestoreServlet.gitblit = gitblit; + } + + + /** + * Handles batch upload request (metadata) + * + * @param request + * @param response + * @throws javax.servlet.ServletException + * @throws java.io.IOException + */ + @Override + protected void doPost(HttpServletRequest request, + HttpServletResponse response) throws ServletException ,IOException { + + UrlInfo info = getInfoFromRequest(request); + if (info == null) { + sendError(response, HttpServletResponse.SC_NOT_FOUND); + return; + } + + //Post is for batch operations so no oid should be defined + if (info.oid != null) { + sendError(response, HttpServletResponse.SC_BAD_REQUEST); + return; + } + + IGitLFS.Batch batch = deserialize(request, response, IGitLFS.Batch.class); + + if (batch == null) { + sendError(response, HttpServletResponse.SC_BAD_REQUEST); + return; + } + + UserModel user = getUserOrAnonymous(request); + + IGitLFS.BatchResponse batchResponse = new IGitLFS.BatchResponse(); + + if (batch.operation.equalsIgnoreCase("upload")) { + for (IGitLFS.Request item : batch.objects) { + + Status state = gitblit.addObject(item.oid, item.size, user, info.repository); + + batchResponse.objects.add(getResponseForUpload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state)); + } + } else if (batch.operation.equalsIgnoreCase("download")) { + for (IGitLFS.Request item : batch.objects) { + + Status state = gitblit.downloadBlob(item.oid, user, info.repository, null); + batchResponse.objects.add(getResponseForDownload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state)); + } + } else { + sendError(response, HttpServletResponse.SC_NOT_IMPLEMENTED); + return; + } + + response.setStatus(HttpServletResponse.SC_OK); + serialize(response, batchResponse); + } + + /** + * Handles the actual upload (BLOB) + * + * @param request + * @param response + * @throws javax.servlet.ServletException + * @throws java.io.IOException + */ + @Override + protected void doPut(HttpServletRequest request, + HttpServletResponse response) throws ServletException ,IOException { + + UrlInfo info = getInfoFromRequest(request); + + if (info == null) { + sendError(response, HttpServletResponse.SC_NOT_FOUND); + return; + } + + //Put is a singular operation so must have oid + if (info.oid == null) { + sendError(response, HttpServletResponse.SC_BAD_REQUEST); + return; + } + + UserModel user = getUserOrAnonymous(request); + long size = FilestoreManager.UNDEFINED_SIZE; + + + + FilestoreModel.Status status = gitblit.uploadBlob(info.oid, size, user, info.repository, request.getInputStream()); + IGitLFS.Response responseObject = getResponseForUpload(info.baseUrl, info.oid, size, user.getName(), info.repository.name, status); + + logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", + "PUT", info.oid, user.getName(), info.repository.name, status.toString() )); + + if (responseObject.error == null) { + response.setStatus(responseObject.successCode); + } else { + serialize(response, responseObject.error); + } + }; + + /** + * Handles a download + * Treated as hypermedia request if accept header contains Git-LFS MIME + * otherwise treated as a download of the blob + * @param request + * @param response + * @throws javax.servlet.ServletException + * @throws java.io.IOException + */ + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException ,IOException { + + UrlInfo info = getInfoFromRequest(request); + + if (info == null || info.oid == null) { + sendError(response, HttpServletResponse.SC_NOT_FOUND); + return; + } + + UserModel user = getUserOrAnonymous(request); + + FilestoreModel model = gitblit.getObject(info.oid, user, info.repository); + long size = FilestoreManager.UNDEFINED_SIZE; + + boolean isMetaRequest = AccessRestrictionFilter.hasContentInRequestHeader(request, "Accept", GIT_LFS_META_MIME); + FilestoreModel.Status status = Status.Unavailable; + + if (model != null) { + size = model.getSize(); + status = model.getStatus(); + } + + if (!isMetaRequest) { + status = gitblit.downloadBlob(info.oid, user, info.repository, response.getOutputStream()); + + logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", + "GET", info.oid, user.getName(), info.repository.name, status.toString() )); + } + + if (status == Status.Error_Unexpected_Stream_End) { + return; + } + + IGitLFS.Response responseObject = getResponseForDownload(info.baseUrl, + info.oid, size, user.getName(), info.repository.name, status); + + if (responseObject.error == null) { + response.setStatus(responseObject.successCode); + + if (isMetaRequest) { + serialize(response, responseObject); + } + } else { + response.setStatus(responseObject.error.code); + serialize(response, responseObject.error); + } + }; + + private void sendError(HttpServletResponse response, int code) throws IOException { + + String msg = ""; + + switch (code) + { + case HttpServletResponse.SC_NOT_FOUND: msg = "Not Found"; break; + case HttpServletResponse.SC_NOT_IMPLEMENTED: msg = "Not Implemented"; break; + case HttpServletResponse.SC_BAD_REQUEST: msg = "Malformed Git-LFS request"; break; + + default: msg = "Unknown Error"; + } + + response.setStatus(code); + serialize(response, new IGitLFS.ObjectError(code, msg)); + } + + @SuppressWarnings("incomplete-switch") + private IGitLFS.Response getResponseForUpload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) { + + switch (state) { + case AuthenticationRequired: + return new IGitLFS.Response(oid, size, 401, MessageFormat.format("Authentication required to write to repository {0}", repo)); + case Error_Unauthorized: + return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have write permissions to repository {1}", user, repo)); + case Error_Exceeds_Size_Limit: + return new IGitLFS.Response(oid, size, 509, MessageFormat.format("Object is larger than allowed limit of {1}", gitblit.getMaxUploadSize())); + case Error_Hash_Mismatch: + return new IGitLFS.Response(oid, size, 422, "Hash mismatch"); + case Error_Invalid_Oid: + return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid)); + case Error_Invalid_Size: + return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid size", size)); + case Error_Size_Mismatch: + return new IGitLFS.Response(oid, size, 422, "Object size mismatch"); + case Deleted: + return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") ); + case Upload_In_Progress: + return new IGitLFS.Response(oid, size, 503, "File currently being uploaded by another user"); + case Unavailable: + return new IGitLFS.Response(oid, size, 404, MessageFormat.format("Repository {0}, does not exist for user {1}", repo, user)); + case Upload_Pending: + return new IGitLFS.Response(oid, size, 202, "upload", getObjectUri(baseUrl, repo, oid) ); + case Available: + return new IGitLFS.Response(oid, size, 200, "upload", getObjectUri(baseUrl, repo, oid) ); + } + + return new IGitLFS.Response(oid, size, 500, "Unknown Error"); + } + + @SuppressWarnings("incomplete-switch") + private IGitLFS.Response getResponseForDownload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) { + + switch (state) { + case Error_Unauthorized: + return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have read permissions to repository {1}", user, repo)); + case Error_Invalid_Oid: + return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid)); + case Error_Unknown: + return new IGitLFS.Response(oid, size, 500, "Unknown Error"); + case Deleted: + return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") ); + case Available: + return new IGitLFS.Response(oid, size, 200, "download", getObjectUri(baseUrl, repo, oid) ); + } + + return new IGitLFS.Response(oid, size, 404, "Object not available"); + } + + + private String getObjectUri(String baseUrl, String repo, String oid) { + return baseUrl + "/" + repo + "/" + Constants.R_LFS + "objects/" + oid; + } + + + protected void serialize(HttpServletResponse response, Object o) throws IOException { + if (o != null) { + // Send JSON response + String json = JsonUtils.toJsonString(o); + response.setCharacterEncoding(Constants.ENCODING); + response.setContentType(GIT_LFS_META_MIME); + response.getWriter().append(json); + } + } + + protected X deserialize(HttpServletRequest request, HttpServletResponse response, + Class clazz) { + + String json = ""; + try { + + json = readJson(request, response); + + return JsonUtils.fromJsonString(json.toString(), clazz); + + } catch (Exception e) { + //Intentional silent fail + } + + return null; + } + + private String readJson(HttpServletRequest request, HttpServletResponse response) + throws IOException { + BufferedReader reader = request.getReader(); + StringBuilder json = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + json.append(line); + } + reader.close(); + + if (json.length() == 0) { + logger.error(MessageFormat.format("Failed to receive json data from {0}", + request.getRemoteAddr())); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + return json.toString(); + } + + private UserModel getUserOrAnonymous(HttpServletRequest r) { + UserModel user = (UserModel) r.getUserPrincipal(); + if (user != null) { return user; } + return UserModel.ANONYMOUS; + } + + private static class UrlInfo { + public RepositoryModel repository; + public String oid; + public String baseUrl; + + public UrlInfo(RepositoryModel repo, String oid, String baseUrl) { + this.repository = repo; + this.oid = oid; + this.baseUrl = baseUrl; + } + } + + public static UrlInfo getInfoFromRequest(HttpServletRequest httpRequest) { + + String url = httpRequest.getRequestURL().toString(); + Pattern p = Pattern.compile(REGEX_PATH); + Matcher m = p.matcher(url); + + + if (m.find()) { + RepositoryModel repo = gitblit.getRepositoryModel(m.group(REGEX_GROUP_REPOSITORY)); + String baseUrl = m.group(REGEX_GROUP_BASE_URI) + "/" + m.group(REGEX_GROUP_PREFIX); + + if (m.group(REGEX_GROUP_ENDPOINT).equals("batch")) { + return new UrlInfo(repo, null, baseUrl); + } else { + return new UrlInfo(repo, m.group(REGEX_GROUP_ENDPOINT), baseUrl); + } + } + + return null; + } + + + public interface IGitLFS { + + @SuppressWarnings("serial") + public class Request implements Serializable + { + public String oid; + public long size; + } + + + @SuppressWarnings("serial") + public class Batch implements Serializable + { + public String operation; + public List objects; + } + + + @SuppressWarnings("serial") + public class Response implements Serializable + { + public String oid; + public long size; + public Map actions; + public ObjectError error; + public transient int successCode; + + public Response(String id, long itemSize, int errorCode, String errorText) { + oid = id; + size = itemSize; + actions = null; + successCode = 0; + error = new ObjectError(errorCode, errorText); + } + + public Response(String id, long itemSize, int actionCode, String action, String uri) { + oid = id; + size = itemSize; + error = null; + successCode = actionCode; + actions = new HashMap(); + actions.put(action, new HyperMediaLink(action, uri)); + } + + } + + @SuppressWarnings("serial") + public class BatchResponse implements Serializable { + public List objects; + + public BatchResponse() { + objects = new ArrayList(); + } + } + + + @SuppressWarnings("serial") + public class ObjectError implements Serializable + { + public String message; + public int code; + public String documentation_url; + public Integer request_id; + + public ObjectError(int errorCode, String errorText) { + code = errorCode; + message = errorText; + request_id = null; + } + } + + @SuppressWarnings("serial") + public class HyperMediaLink implements Serializable + { + public String href; + public transient String header; + //public Date expires_at; + + public HyperMediaLink(String action, String uri) { + header = action; + href = uri; + } + } + } + + + +} diff --git a/src/main/java/com/gitblit/servlet/GitFilter.java b/src/main/java/com/gitblit/servlet/GitFilter.java index b29fdb6a..27408f02 100644 --- a/src/main/java/com/gitblit/servlet/GitFilter.java +++ b/src/main/java/com/gitblit/servlet/GitFilter.java @@ -19,6 +19,7 @@ import java.text.MessageFormat; import com.google.inject.Inject; import com.google.inject.Singleton; + import javax.servlet.http.HttpServletRequest; import com.gitblit.Constants.AccessRestrictionType; @@ -48,9 +49,11 @@ public class GitFilter extends AccessRestrictionFilter { protected static final String gitReceivePack = "/git-receive-pack"; protected static final String gitUploadPack = "/git-upload-pack"; - + + protected static final String gitLfs = "/info/lfs"; + protected static final String[] suffixes = { gitReceivePack, gitUploadPack, "/info/refs", "/HEAD", - "/objects" }; + "/objects", gitLfs }; private IStoredSettings settings; @@ -116,6 +119,8 @@ public class GitFilter extends AccessRestrictionFilter { return gitReceivePack; } else if (suffix.contains("?service=git-upload-pack")) { return gitUploadPack; + } else if (suffix.startsWith(gitLfs)) { + return gitLfs; } else { return gitUploadPack; } @@ -144,7 +149,13 @@ public class GitFilter extends AccessRestrictionFilter { * @return true if the server allows repository creation on-push */ @Override - protected boolean isCreationAllowed() { + protected boolean isCreationAllowed(String action) { + + //Repository must already exist before large files can be deposited + if (action.equals(gitLfs)) { + return false; + } + return settings.getBoolean(Keys.git.allowCreateOnPush, true); } @@ -156,9 +167,15 @@ public class GitFilter extends AccessRestrictionFilter { * @return true if the action may be performed */ @Override - protected boolean isActionAllowed(RepositoryModel repository, String action) { + protected boolean isActionAllowed(RepositoryModel repository, String action, String method) { // the log here has been moved into ReceiveHook to provide clients with // error messages + if (gitLfs.equals(action)) { + if (!method.matches("GET|POST|PUT|HEAD")) { + return false; + } + } + return true; } @@ -172,16 +189,25 @@ public class GitFilter extends AccessRestrictionFilter { * * @param repository * @param action + * @param method * @return true if authentication required */ @Override - protected boolean requiresAuthentication(RepositoryModel repository, String action) { + protected boolean requiresAuthentication(RepositoryModel repository, String action, String method) { if (gitUploadPack.equals(action)) { // send to client return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE); } else if (gitReceivePack.equals(action)) { // receive from client return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH); + } else if (gitLfs.equals(action)) { + + if (method.matches("GET|HEAD")) { + return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE); + } else { + //NOTE: Treat POST as PUT as as without reading message type cannot determine + return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH); + } } return false; } @@ -230,6 +256,12 @@ public class GitFilter extends AccessRestrictionFilter { @Override protected RepositoryModel createRepository(UserModel user, String repository, String action) { boolean isPush = !StringUtils.isEmpty(action) && gitReceivePack.equals(action); + + if (action.equals(gitLfs)) { + //Repository must already exist for any filestore actions + return null; + } + if (isPush) { if (user.canCreate(repository)) { // user is pushing to a new repository @@ -281,4 +313,40 @@ public class GitFilter extends AccessRestrictionFilter { // repository could not be created or action was not a push return null; } + + /** + * Git lfs action uses an alternative authentication header, + * + * @param action + * @return + */ + @Override + protected String getAuthenticationHeader(String action) { + + if (action.equals(gitLfs)) { + return "LFS-Authenticate"; + } + + return super.getAuthenticationHeader(action); + } + + /** + * Interrogates the request headers based on the action + * @param action + * @param request + * @return + */ + @Override + protected boolean hasValidRequestHeader(String action, + HttpServletRequest request) { + + if (action.equals(gitLfs) && request.getMethod().equals("POST")) { + if ( !hasContentInRequestHeader(request, "Accept", FilestoreServlet.GIT_LFS_META_MIME) + || !hasContentInRequestHeader(request, "Content-Type", FilestoreServlet.GIT_LFS_META_MIME)) { + return false; + } + } + + return super.hasValidRequestHeader(action, request); + } } diff --git a/src/main/java/com/gitblit/servlet/GitblitContext.java b/src/main/java/com/gitblit/servlet/GitblitContext.java index 077624c2..fb8f6b9d 100644 --- a/src/main/java/com/gitblit/servlet/GitblitContext.java +++ b/src/main/java/com/gitblit/servlet/GitblitContext.java @@ -44,6 +44,7 @@ import com.gitblit.guice.CoreModule; import com.gitblit.guice.WebModule; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; +import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.IGitblit; import com.gitblit.manager.IManager; import com.gitblit.manager.INotificationManager; @@ -204,6 +205,7 @@ public class GitblitContext extends GuiceServletContextListener { startManager(injector, ITicketService.class); startManager(injector, IGitblit.class); startManager(injector, IServicesManager.class); + startManager(injector, IFilestoreManager.class); // start the plugin manager last so that plugins can depend on // deterministic access to all other managers in their start() methods diff --git a/src/main/java/com/gitblit/servlet/RawFilter.java b/src/main/java/com/gitblit/servlet/RawFilter.java index fe4af040..8913a197 100644 --- a/src/main/java/com/gitblit/servlet/RawFilter.java +++ b/src/main/java/com/gitblit/servlet/RawFilter.java @@ -98,7 +98,7 @@ public class RawFilter extends AccessRestrictionFilter { * @return true if the filter allows repository creation */ @Override - protected boolean isCreationAllowed() { + protected boolean isCreationAllowed(String action) { return false; } @@ -107,10 +107,11 @@ public class RawFilter extends AccessRestrictionFilter { * * @param repository * @param action + * @param method * @return true if the action may be performed */ @Override - protected boolean isActionAllowed(RepositoryModel repository, String action) { + protected boolean isActionAllowed(RepositoryModel repository, String action, String method) { return true; } @@ -119,10 +120,11 @@ public class RawFilter extends AccessRestrictionFilter { * * @param repository * @param action + * @param method * @return true if authentication required */ @Override - protected boolean requiresAuthentication(RepositoryModel repository, String action) { + protected boolean requiresAuthentication(RepositoryModel repository, String action, String method) { return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW); } diff --git a/src/main/java/com/gitblit/utils/JsonUtils.java b/src/main/java/com/gitblit/utils/JsonUtils.java index be7148cb..f389776b 100644 --- a/src/main/java/com/gitblit/utils/JsonUtils.java +++ b/src/main/java/com/gitblit/utils/JsonUtils.java @@ -46,6 +46,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; @@ -79,23 +80,29 @@ public class JsonUtils { /** * Convert a json string to an object of the specified type. - * + * * @param json * @param clazz - * @return an object + * @return the deserialized object + * @throws JsonParseException + * @throws JsonSyntaxException */ - public static X fromJsonString(String json, Class clazz) { + public static X fromJsonString(String json, Class clazz) throws JsonParseException, + JsonSyntaxException { return gson().fromJson(json, clazz); } /** * Convert a json string to an object of the specified type. - * + * * @param json - * @param clazz - * @return an object + * @param type + * @return the deserialized object + * @throws JsonParseException + * @throws JsonSyntaxException */ - public static X fromJsonString(String json, Type type) { + public static X fromJsonString(String json, Type type) throws JsonParseException, + JsonSyntaxException { return gson().fromJson(json, type); } diff --git a/src/main/java/com/gitblit/wicket/FilestoreUI.java b/src/main/java/com/gitblit/wicket/FilestoreUI.java new file mode 100644 index 00000000..8837ba18 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/FilestoreUI.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.wicket; + +import org.apache.wicket.markup.html.basic.Label; + +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.FilestoreModel.Status; + +/** + * Common filestore ui methods and classes. + * + * @author Paul Martin + * + */ +public class FilestoreUI { + + public static Label getStatusIcon(String wicketId, FilestoreModel item) { + return getStatusIcon(wicketId, item.getStatus()); + } + + public static Label getStatusIcon(String wicketId, Status status) { + Label label = new Label(wicketId); + + switch (status) { + case Upload_Pending: + WicketUtils.setCssClass(label, "fa fa-spinner fa-fw file-negative"); + break; + case Upload_In_Progress: + WicketUtils.setCssClass(label, "fa fa-spinner fa-spin fa-fw file-positive"); + break; + case Available: + WicketUtils.setCssClass(label, "fa fa-check fa-fw file-positive"); + break; + case Deleted: + WicketUtils.setCssClass(label, "fa fa-ban fa-fw file-negative"); + break; + case Unavailable: + WicketUtils.setCssClass(label, "fa fa-times fa-fw file-negative"); + break; + default: + WicketUtils.setCssClass(label, "fa fa-exclamation-triangle fa-fw file-negative"); + } + WicketUtils.setHtmlTooltip(label, status.toString()); + + return label; + } + +} diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index 359040b5..296c2544 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -37,6 +37,7 @@ import com.gitblit.Keys; import com.gitblit.extensions.GitblitWicketPlugin; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; +import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.IGitblit; import com.gitblit.manager.INotificationManager; import com.gitblit.manager.IPluginManager; @@ -63,6 +64,7 @@ import com.gitblit.wicket.pages.EditRepositoryPage; import com.gitblit.wicket.pages.EditTicketPage; import com.gitblit.wicket.pages.ExportTicketPage; import com.gitblit.wicket.pages.FederationRegistrationPage; +import com.gitblit.wicket.pages.FilestorePage; import com.gitblit.wicket.pages.ForkPage; import com.gitblit.wicket.pages.ForksPage; import com.gitblit.wicket.pages.GitSearchPage; @@ -131,6 +133,8 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { private final IGitblit gitblit; private final IServicesManager services; + + private final IFilestoreManager filestoreManager; @Inject public GitBlitWebApp( @@ -145,7 +149,8 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { IProjectManager projectManager, IFederationManager federationManager, IGitblit gitblit, - IServicesManager services) { + IServicesManager services, + IFilestoreManager filestoreManager) { super(); this.publicKeyManagerProvider = publicKeyManagerProvider; @@ -162,6 +167,7 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { this.federationManager = federationManager; this.gitblit = gitblit; this.services = services; + this.filestoreManager = filestoreManager; } @Override @@ -238,6 +244,9 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { mount("/user", UserPage.class, "user"); mount("/forks", ForksPage.class, "r"); mount("/fork", ForkPage.class, "r"); + + // filestore URL + mount("/filestore", FilestorePage.class); // allow started Wicket plugins to initialize for (PluginWrapper pluginWrapper : pluginManager.getPlugins()) { @@ -476,4 +485,9 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp { public static GitBlitWebApp get() { return (GitBlitWebApp) WebApplication.get(); } + + @Override + public IFilestoreManager filestore() { + return filestoreManager; + } } diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index d8027548..36c416e7 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -764,4 +764,10 @@ gb.deleteRepositoryHeader = Delete Repository gb.deleteRepositoryDescription = Deleted repositories will be unrecoverable. gb.show_whitespace = show whitespace gb.ignore_whitespace = ignore whitespace -gb.allRepositories = All Repositories \ No newline at end of file +gb.allRepositories = All Repositories +gb.oid = object id +gb.filestore = filestore +gb.filestoreStats = Filestore contains {0} files with a total size of {1}. ({2} remaining) +gb.statusChangedOn = status changed on +gb.statusChangedBy = status changed by +gb.filestoreHelp = How to use the Filestore? \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java index 3041c5da..fefa0f4a 100644 --- a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java +++ b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java @@ -8,6 +8,7 @@ import org.apache.wicket.markup.html.WebPage; import com.gitblit.IStoredSettings; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; +import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.IGitblit; import com.gitblit.manager.INotificationManager; import com.gitblit.manager.IPluginManager; @@ -74,5 +75,7 @@ public interface GitblitWicketApp { public abstract ITicketService tickets(); public abstract TimeZone getTimezone(); + + public abstract IFilestoreManager filestore(); } \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/FilestorePage.html b/src/main/java/com/gitblit/wicket/pages/FilestorePage.html new file mode 100644 index 00000000..e373e704 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/FilestorePage.html @@ -0,0 +1,37 @@ + + + + + +
+ +
+ [repositories message] + [help message] +
+ + + + + + + + + + + + + + + + + + +
[Object status][changedOn][changedBy][Object ID][file size]
[Object state]
[changedOn][changedBy][Object ID][file size]
+
+
+ + \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/FilestorePage.java b/src/main/java/com/gitblit/wicket/pages/FilestorePage.java new file mode 100644 index 00000000..5f103edd --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/FilestorePage.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.wicket.pages; + +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.wicket.Component; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.link.BookmarkablePageLink; +import org.apache.wicket.markup.repeater.Item; +import org.apache.wicket.markup.repeater.data.DataView; +import org.apache.wicket.markup.repeater.data.ListDataProvider; + +import com.gitblit.Constants; +import com.gitblit.Keys; +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.UserModel; +import com.gitblit.wicket.FilestoreUI; +import com.gitblit.wicket.GitBlitWebSession; +import com.gitblit.wicket.WicketUtils; + +/** + * Page to display the current status of the filestore. + * Certain errors also displayed to aid in fault finding + * + * @author Paul Martin + * + * + */ +public class FilestorePage extends RootPage { + + public FilestorePage() { + super(); + setupPage("", ""); + // check to see if we should display a login message + boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, true); + if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) { + String messageSource = app().settings().getString(Keys.web.loginMessage, "gitblit"); + return; + } + + final List files = app().filestore().getAllObjects(); + final long nBytesUsed = app().filestore().getFilestoreUsedByteCount(); + final long nBytesAvailable = app().filestore().getFilestoreAvailableByteCount(); + + // Load the markdown welcome message + String messageSource = app().settings().getString(Keys.web.repositoriesMessage, "gitblit"); + String message = MessageFormat.format(getString("gb.filestoreStats"), files.size(), + FileUtils.byteCountToDisplaySize(nBytesUsed), FileUtils.byteCountToDisplaySize(nBytesAvailable) ); + + Component repositoriesMessage = new Label("repositoriesMessage", message) + .setEscapeModelStrings(false).setVisible(message.length() > 0); + + add(repositoriesMessage); + + BookmarkablePageLink helpLink = new BookmarkablePageLink("filestoreHelp", FilestoreUsage.class); + helpLink.add(new Label("helpMessage", getString("gb.filestoreHelp"))); + add(helpLink); + + + DataView filesView = new DataView("fileRow", + new ListDataProvider(files)) { + private static final long serialVersionUID = 1L; + private int counter; + + @Override + protected void onBeforeRender() { + super.onBeforeRender(); + counter = 0; + } + + @Override + public void populateItem(final Item item) { + final FilestoreModel entry = item.getModelObject(); + + DateFormat dateFormater = new SimpleDateFormat(Constants.ISO8601); + + UserModel user = app().users().getUserModel(entry.getChangedBy()); + user = user == null ? UserModel.ANONYMOUS : user; + + Label icon = FilestoreUI.getStatusIcon("status", entry); + item.add(icon); + item.add(new Label("on", dateFormater.format(entry.getChangedOn()))); + item.add(new Label("by", user.getDisplayName())); + + item.add(new Label("oid", entry.oid)); + item.add(new Label("size", FileUtils.byteCountToDisplaySize(entry.getSize()))); + + WicketUtils.setAlternatingBackground(item, counter); + counter++; + } + + }; + + add(filesView); + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.html b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.html new file mode 100644 index 00000000..e9bff47c --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.html @@ -0,0 +1,69 @@ + + + + + +
+
+
+
+ +
+

Using the Filestore

+

+ All clients intending to use the filestore must first install the Git-LFS Client and then run git lfs init to register the hooks globally.
+ This version of GitBlit has been verified with Git-LFS client version 0.6.0 which requires Git v1.8.2 or higher. +

+
+ +

Clone

+

+ Just git clone as usual, no further action is required as GitBlit is configured to use the default Git-LFS end point {repository}/info/lfs/objects/.
+ If the repository uses a 3rd party Git-LFS server you will need to manually configure the correct endpoints. +

+ +

Add

+

After configuring the file types or paths to be tracked using git lfs track "*.bin" just add files as usual with git add command.
+ Tracked files can also be configured manually using the .gitattributes file.

+ +

Remove

+

When you remove a Git-LFS tracked file only the pointer file will be removed from your repository.
+ All files remain on the server to allow previous versions to be checked out. +

+ +

Learn more...

+

See the current Git-LFS specification for further details.

+
+ +
+

Limitations & Warnings

+

GitBlit currently provides a server-only implementation of the opensource Git-LFS API, other implementations are available.
+ However, until JGit provides Git-LFS client capabilities some GitBlit features may not be fully supported when using the filestore. + Notably: +

    +
  • Mirroring a repository that uses Git-LFS - Only the pointer files, not the large files, are mirrored.
  • +
  • Federation - Only the pointer files, not the large files, are transfered.
  • +
+

+
+ +
+

GitBlit Configuration

+

GitBlit provides the following configuration items when using the filestore: +

filestore.storageFolder

+

Defines the path on the server where filestore objects are to be saved. This defaults to ${baseFolder}/lfs

+

filestore.maxUploadSize

+

Defines the maximum allowable size that can be uploaded to the filestore. Once a file is uploaded it will be unaffected by later changes in this property. This defaults to -1 indicating no limits.

+

+
+ +
+
+
+
+
+ + diff --git a/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.java b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.java new file mode 100644 index 00000000..9bd8e55d --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.wicket.pages; + +public class FilestoreUsage extends RootSubPage { + + public FilestoreUsage() { + super(); + setupPage("", ""); + } + +} diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.java b/src/main/java/com/gitblit/wicket/pages/RootPage.java index 79a4fc67..93d44fc7 100644 --- a/src/main/java/com/gitblit/wicket/pages/RootPage.java +++ b/src/main/java/com/gitblit/wicket/pages/RootPage.java @@ -191,6 +191,7 @@ public abstract class RootPage extends BasePage { } navLinks.add(new PageNavLink("gb.repositories", RepositoriesPage.class, getRootPageParameters())); + navLinks.add(new PageNavLink("gb.filestore", FilestorePage.class, getRootPageParameters())); navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters())); if (allowLucene) { navLinks.add(new PageNavLink("gb.search", LuceneSearchPage.class)); -- cgit v1.2.3