diff --git a/.gitignore b/.gitignore index 5681c47f2..8899c1a95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ tmp .idea* *.iml dist -public/uploadFiles uploads repo/ RUNNING_PID diff --git a/README.md b/README.md index cebf86739..06bb6f546 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,47 @@ License -- Copyright 2015 NAVER Corp. under the Apache License, Version 2.0 -## How to install +How to install +-------------- -### Check JDK version +### Install from binary + +#### Install + +Download the latest version of Yobi from http://yobi.io/yobi.zip and unzip it. +If you have wget and unzip: + + wget http://yobi.io/yobi.zip + unzip yobi.zip + +#### Start + +Go the directory and start Yobi. If the directory is yobi-1.0.0: + + cd yobi-1.0.0 + bin/yobi + +**Note**: If you are using Windows, run "bin/yobi.bat" instead of "bin/yobi". + +Open http://127.0.0.1:9000 with your web browser then you can see the welcome +page. + +#### Upgrade + +Download the latest version of Yobi and unzip it. + +**Note: Don't overwrite or delete `yobi.h2.db` file, `repo` & `uploads` directory!** + +### Build from source + +#### Check JDK version java -version javac -version JDK version 7(1.7) or 8(1.8) is required. -### Download Play Activator +#### Download Play Activator curl -O http://downloads.typesafe.com/typesafe-activator/1.2.10/typesafe-activator-1.2.10-minimal.zip @@ -43,15 +74,15 @@ or using web browser (for windows) http://downloads.typesafe.com/typesafe-activator/1.2.10/typesafe-activator-1.2.10-minimal.zip -### Unzip +#### Unzip unzip typesafe-activator-1.2.10-minimal.zip -### Change directory to unzipped directory +#### Change directory to unzipped directory cd typesafe-activator-1.2.10-minimal -### Download Yobi +#### Download Yobi Case1. using [git client](http://git-scm.com/) (recommended) @@ -68,11 +99,11 @@ If you want to download one of the stable releases, you can download a compresse > You can locate your own Yobi directory in any other place. Please note that you must add Play Activator home path to $PATH environment in that case. -### Change directory to cloned Yobi directory (or cd to your unzipped file directory) +#### Change directory to cloned Yobi directory (or cd to your unzipped file directory) cd yobi -### Run Play Activator +#### Run Play Activator ../activator @@ -83,7 +114,7 @@ or (for windows) Required files will be download automatically. In the first time, it may take about 10 min or more. -### Type start command in console +#### Type start command in console start -DapplyEvolutions.default=true -Dhttp.port=9000 @@ -91,12 +122,7 @@ It will downloaded addtional files and compile sources. If you want to run Yobi in development mode, use **run**. You can see more detailed errors and can use dynamic compilation. -Also, you can configure start options. -If your system's memory is over than 4G, we recommend to use follow options. - - _JAVA_OPTIONS="-Xmx2048m -Xms1024m" activator "start -DapplyEvolutions.default=true -Dhttp.port=9000" - -### Connect with browser +#### Connect with browser http://127.0.0.1:9000 @@ -104,7 +130,7 @@ If you want to change port, check your permission to use 80 port See [http://www.playframework.com/documentation/2.3.6/Production](http://www.playframework.com/documentation/2.3.6/Production) -### Upgrade Yobi +#### Upgrade Yobi Case1. using git client (recommended) In installed directory, just type git update command. @@ -119,6 +145,20 @@ In installed directory, download latest release file and unzip it. ** Be careful! Don't overwrite or delete `yobi.h2.db` file, `repo` & `uploads` directory! ** +### Options + +When start yobi, You can specify the home directory that data and configuration +to read from and store into. If you want to use `/home/user/.yobi` as the home +directory, set 'yobi.home' property as follows: + + bin/yobi -Dyobi.home=/home/user/.yobi + +You can also specify Java options with `_JAVA_OPTIONS` environment variable. If +the memory of your system equals to or greater than 4GB, we recommend to start +Yobi as follows: + + _JAVA_OPTIONS="-Xmx2048m -Xms1024m" activator "start -DapplyEvolutions.default=true -Dhttp.port=9000" + ### Backup Copy the below file and directories to another place. @@ -159,14 +199,43 @@ Yobi는 Apache 2.0 라이선스로 제공됩니다. ## 설치하기 -### JDK version 확인 +### 다운받아 설치하기 + +#### 설치 + +Yobi 최신 버전(http://yobi.io/yobi.zip)을 다운받아서 압축을 풉니다. 예를 들어 +wget으로 받아서 unzip으로 압축을 푼다면: + + wget http://yobi.io/yobi.zip + unzip yobi.zip + +#### 실행 + +압축이 풀린 디렉토리로 이동해서 yobi를 실행합니다. 디렉토리가 yobi-1.0.0 이라면: + + cd yobi-1.0.0 + bin/yobi + +**주의**: 윈도우 사용자는 bin/yobi 대신 bin/yobi.bat을 실행해야합니다. + +이제 웹 브라우저로 http://127.0.0.1:9000 에 접속하면 환영 페이지를 보실 수 있습니다. + +#### 업그레이드 + +설치할 때와 똑같이, 최신 버전을 내려받아 Yobi가 설치된 디렉터리에 압축파일을 +풉니다. **주의사항! `yobi.h2.db` 파일, `repo`와 `uploads` 디렉터리를 삭제하거나 +덮어쓰지 않도록 주의하세요!** + +### 소스 코드에서 빌드하기 + +#### JDK version 확인 java -version javac -version JDK 7(1.7) 혹은 8(1.8) 이어야 합니다. -### Play Activator 내려 받기 +#### Play Activator 내려 받기 curl -O http://downloads.typesafe.com/typesafe-activator/1.2.10/typesafe-activator-1.2.10-minimal.zip @@ -178,15 +247,15 @@ JDK 7(1.7) 혹은 8(1.8) 이어야 합니다. http://downloads.typesafe.com/typesafe-activator/1.2.10/typesafe-activator-1.2.10-minimal.zip -### 압축풀기 +#### 압축풀기 unzip typesafe-activator-1.2.10-minimal.zip -### 압축을 푼 다음 하위 디렉터리로 이동 +#### 압축을 푼 다음 하위 디렉터리로 이동 cd typesafe-activator-1.2.10-minimal -### Yobi 소스 내려 받기 +#### Yobi 소스 내려 받기 case1. [git 클라이언트](http://git-scm.com)를 이용한 다운로드 (추천) @@ -194,18 +263,18 @@ case1. [git 클라이언트](http://git-scm.com)를 이용한 다운로드 (추 case2. 단순히 최신 안정버전을 내려받고자 할 때는 아래 링크를 이용해서 압축파일을 내려받은 다음 yobi를 폴더이름으로해서 해제합니다. - https://github.com/naver/yobi/archive/master.zip + git pull https://github.com/naver/yobi.git master 주의! case2의 경우, 업그레이드를 할 때 문제가 생길 수 있습니다. > 임의의 장소에 Yobi 디렉터리를 위치시킬 경우에는 activator 실행파일이 있는 Play Activator 디렉터리를 $PATH 환경변수에 추가해 주세요. -### clone 받은 Yobi 디렉터리로 이동 +#### clone 받은 Yobi 디렉터리로 이동 (혹은 압축을 해제한 디렉터리로 이동) cd yobi -### 상단에 있는 activator 실행파일 실행 +#### 상단에 있는 activator 실행파일 실행 ../activator @@ -215,27 +284,21 @@ case2. 단순히 최신 안정버전을 내려받고자 할 때는 아래 링크 실행하면 필요한 파일들을 web에서 내려받습니다. 첫 실행시 네트워크 상황에 따라 10여분 가까이 소요될 수 있습니다. -### 콘솔이 뜨면 start 명령어로 기동 +#### 콘솔이 뜨면 start 명령어로 기동 start -DapplyEvolutions.default=true -Dhttp.port=9000 추가로 필요한 파일들을 web에서 내려받은 다음 소스 파일들을 컴파일 후 운영 모드(production mode)로 실행합니다. 개발 모드(development mode)로 실행하고자 할 경우에는 **start** 명령어 대신에 **run** 명령어로 실행합니다. -시작 옵션은 조정가능합니다. 만약 시스템 메모리가 4기가 이상이라면 -아래 옵션으로 실행하는걸 권장합니다. - - _JAVA_OPTIONS="-Xmx2048m -Xms1024m" activator "start -DapplyEvolutions.default=true -Dhttp.port=9000" - - -### 브라우저로 접속 +#### 브라우저로 접속 http://127.0.0.1:9000 80 포트 등으로 포트를 변경하고 싶을 경우에는 해당 포트가 사용가능한지 확인 한 다음 80 포트를 사용할 수 있는 계정으로 실행합니다. 관련해서는 [http://www.playframework.com/documentation/2.3.6/Production](http://www.playframework.com/documentation/2.3.6/Production) 부분을 확인해 주세요. -### 업그레이드 하기 +#### 업그레이드 하기 case1. git 클라이언트를 이용 (추천) 설치된 디렉터리에서, 아래와 같은 git 명령어를 이용합니다 @@ -250,6 +313,18 @@ case2. 압축파일을 내려받을 경우 **주의사항! `yobi.h2.db` 파일, `repo`와 `uploads` 디렉터리를 삭제하거나 덮어쓰지 않도록 주의하세요!** +### 옵션 + +Yobi가 파일을 불러오고 저장할 홈 디렉토리를 `yobi.home` 속성을 통해 지정할 +수 있습니다. 예를 들어, /home/user/.yobi를 홈 디렉토리로 사용하려면 Yobi를 +시작할 때 다음과 같이 yobi.home 프로퍼티를 지정합니다. + + bin/yobi -Dyobi.home=/home/user/.yobi + +`_JAVA_OPTIONS` 환경변수를 이용해 자바 옵션을 지정할 수도 있습니다. 시스템 +메모리가 4기가 이상이라면, 다음과 같은 옵션으로 실행하는걸 권장합니다. + + _JAVA_OPTIONS="-Xmx2048m -Xms1024m" activator "start -DapplyEvolutions.default=true -Dhttp.port=9000" ### 백업하기 diff --git a/app/Global.java b/app/Global.java index 1ded38568..68c846ced 100644 --- a/app/Global.java +++ b/app/Global.java @@ -19,35 +19,40 @@ * limitations under the License. */ -import mailbox.MailboxService; import com.avaje.ebean.Ebean; +import com.typesafe.config.ConfigFactory; import controllers.SvnApp; import controllers.UserApp; import controllers.routes; +import mailbox.MailboxService; import models.*; import org.apache.commons.lang3.StringUtils; import org.apache.http.impl.cookie.DateUtils; import play.Application; +import play.Configuration; import play.GlobalSettings; import play.Play; import play.api.mvc.Handler; import play.data.Form; +import play.libs.F.Promise; import play.mvc.Action; import play.mvc.Http; import play.mvc.Http.RequestHeader; -import play.mvc.Result; -import play.libs.F.Promise; - import play.mvc.Result; import play.mvc.Results; import utils.*; import views.html.welcome.restart; import views.html.welcome.secret; +import javax.annotation.Nonnull; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Method; import java.math.BigInteger; import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -64,8 +69,67 @@ public class Global extends GlobalSettings { private boolean isSecretInvalid = false; private boolean isRestartRequired = false; - private MailboxService mailboxService = new MailboxService(); + private boolean hasFailedToUpdateSecretKey = false; + + private ConfigFile configFile = new ConfigFile("config", "application.conf"); + private ConfigFile loggerConfigFile = new ConfigFile("logger", "application-logger.xml"); + + @Override + public Configuration onLoadConfig(play.Configuration config, File path, ClassLoader classloader) { + initLoggerConfig(); + return initConfig(classloader); + } + + /** + * Creates application.xml by default if necessary + * + * @param classloader + * @return the configuration read from the created file, + * or null if this method didn't create the file. + */ + private Configuration initConfig(ClassLoader classloader) { + if (configFile.isLocationSpecified()) { + return null; + } + + try { + if (configFile.getPath().toFile().exists()) { + return null; + } + } catch (URISyntaxException e) { + play.Logger.error("Failed to check whether the config file exists", e); + return null; + } + + try { + configFile.createByDefault(); + return new Configuration(ConfigFactory.load(classloader, + ConfigFactory.parseFileAnySyntax(configFile.getPath().toFile()))); + } catch (Exception e) { + play.Logger.error("Failed to initialize configuration", e); + return null; + } + } + + /** + * Creates application-logger.xml by default if necessary + * + * Note: This method creates application-logger.xml even if logger.xml exists. + */ + private void initLoggerConfig() { + try { + if (!loggerConfigFile.isLocationSpecified() && !loggerConfigFile.getPath().toFile().exists()) { + try { + loggerConfigFile.createByDefault(); + } catch (Exception e) { + play.Logger.error("Failed to initialize logger configuration", e); + } + } + } catch (URISyntaxException e) { + play.Logger.error("Failed to check whether the logger config file exists", e); + } + } @Override public void onStart(Application app) { @@ -78,9 +142,12 @@ public void onStart(Application app) { NotificationMail.onStart(); NotificationEvent.onStart(); Attachment.onStart(); - YobiUpdate.onStart(); AccessControl.onStart(); - mailboxService.start(); + + if (!isSecretInvalid) { + YobiUpdate.onStart(); + mailboxService.start(); + } } private boolean equalsDefaultSecret() { @@ -126,7 +193,7 @@ private Action getRestartAction() { return new Action.Simple() { @Override public Promise call(Http.Context ctx) throws Throwable { - return Promise.pure((Result) ok(restart.render())); + return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey))); } }; } @@ -143,9 +210,14 @@ public Promise call(Http.Context ctx) throws Throwable { } User siteAdmin = SiteAdmin.updateDefaultSiteAdmin(newSiteAdminUserForm.get()); - replaceSiteSecretKey(createSeed(siteAdmin.password)); + try { + updateSiteSecretKey(createSeed(siteAdmin.loginId + ":" + siteAdmin.password)); + } catch (Exception e) { + play.Logger.warn("Failed to update secret key", e); + hasFailedToUpdateSecretKey = true; + } isRestartRequired = true; - return Promise.pure((Result) ok(restart.render())); + return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey))); } else { return Promise.pure((Result) ok(secret.render(SiteAdmin.SITEADMIN_DEFAULT_LOGINID, new Form<>(User.class)))); } @@ -161,15 +233,18 @@ private String createSeed(String basicSeed) { return seed; } - private void replaceSiteSecretKey(String seed) throws IOException { + private void updateSiteSecretKey(String seed) throws Exception { SecureRandom random = new SecureRandom(seed.getBytes(Config.getCharset())); String secret = new BigInteger(130, random).toString(32); - Path path = Paths.get("conf/application.conf"); - byte[] bytes = Files.readAllBytes(path); + if (configFile.isExternal()) { + throw new Exception("Cowardly refusing to update an external file: " + configFile.getPath()); + } + + byte[] bytes = Files.readAllBytes(configFile.getPath()); String config = new String(bytes, Config.getCharset()); config = config.replace(DEFAULT_SECRET, secret); - Files.write(path, config.getBytes(Config.getCharset())); + Files.write(configFile.getPath(), config.getBytes(Config.getCharset())); } private boolean hasError(Form newUserForm) { @@ -240,4 +315,75 @@ public Promise onBadRequest(RequestHeader request, String error) { return Promise.pure((Result) badRequest(ErrorViews.BadRequest.render())); } + private static class ConfigFile { + private static final String CONFIG_DIRNAME = "conf"; + private final String fileName; + private final String defaultFileName; + private final String propertyGroup; + + ConfigFile(String propertyGroup, String fileName) { + this.propertyGroup = propertyGroup; + this.fileName = fileName; + this.defaultFileName = fileName + ".default"; + } + + String getProperty(@Nonnull String key) { + return System.getProperty(propertyGroup + "." + key); + } + + String getProperty(@Nonnull String key, String defaultValue) { + return System.getProperty(propertyGroup + "." + key, defaultValue); + } + + /** + * The location of the config file is specified by user + */ + boolean isLocationSpecified() { + return (getProperty("resource") != null) + || (getProperty("file") != null) + || (getProperty("url") != null); + } + + void createByDefault() throws IOException, URISyntaxException { + InputStream stream = Config.class.getClassLoader().getResourceAsStream(defaultFileName); + + getPath().toFile().getParentFile().mkdirs(); + + if (stream != null) { + Files.copy(stream, getPath()); + } else { + Files.copy(getDirectoryPath().resolve(defaultFileName), getPath()); + } + } + + /** + * @return the path to the configuration file + * @throws java.lang.IllegalStateException + */ + Path getPath() throws URISyntaxException { + if (getProperty("url") != null) { + return Paths.get(new URI(getProperty("url"))); + } + + if (getProperty("file") != null) { + return Paths.get(getProperty("file")); + } + + String filename = getProperty("resource", fileName); + + return getDirectoryPath().resolve(filename); + } + + /** + * @return the path to the directory to store configuration files + */ + static Path getDirectoryPath() { + return Paths.get(Config.getYobiHome(""), CONFIG_DIRNAME); + } + + boolean isExternal() throws IOException, URISyntaxException { + return !FileUtil.isSubpathOf(getPath(), getDirectoryPath()) && + !FileUtil.isSubpathOf(getPath(), Paths.get(Config.getYobiHome())); + } + } } diff --git a/app/controllers/Application.java b/app/controllers/Application.java index bb4ed825a..5580a3681 100644 --- a/app/controllers/Application.java +++ b/app/controllers/Application.java @@ -52,7 +52,6 @@ public static Result removeTrailer(String paths){ } public static Result init() { - makeUploadFolder(); makeTestRepository(); return redirect(routes.Application.index()); } @@ -63,10 +62,6 @@ public static Result jsMessages() { return ok(messages.generate("Messages")).as("application/javascript"); } - private static void makeUploadFolder() { - new File("public/uploadFiles/").mkdir(); - } - private static void makeTestRepository() { for (Project project : Project.find.all()) { Logger.debug("makeTestRepository: " + project.name); diff --git a/app/controllers/ImportApp.java b/app/controllers/ImportApp.java index 715cd0fd7..0a5f97f65 100644 --- a/app/controllers/ImportApp.java +++ b/app/controllers/ImportApp.java @@ -109,7 +109,7 @@ public static Result newProject() throws Exception { if (!filledNewProjectForm.errors().isEmpty()) { List orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); - FileUtil.rm_rf(new File(GitRepository.getGitDirectory(project))); + FileUtil.rm_rf(GitRepository.getGitDirectory(project)); return badRequest(importing.render("title.newProject", filledNewProjectForm, orgUserList)); } else { return redirect(routes.ProjectApp.project(project.owner, project.name)); diff --git a/app/models/Attachment.java b/app/models/Attachment.java index 3db042b58..c739c116e 100644 --- a/app/models/Attachment.java +++ b/app/models/Attachment.java @@ -33,6 +33,7 @@ import play.libs.Akka; import scala.concurrent.duration.Duration; import utils.AttachmentCache; +import utils.Config; import utils.FileUtil; import utils.JodaDateUtil; @@ -277,7 +278,11 @@ public boolean store(File file, String name, Resource container) throws IOExcept * @return the file */ public File getFile() { - return new File(uploadDirectory, this.hash); + return new File(getUploadDirectory(), this.hash); + } + + public static File getUploadDirectory() { + return new File(utils.Config.getYobiHome(), uploadDirectory); } /** @@ -300,7 +305,7 @@ public static void setUploadDirectory(String path) { * @return true if the file exists */ public static boolean fileExists(String hash) { - return new File(uploadDirectory, hash).isFile(); + return new File(getUploadDirectory(), hash).isFile(); } /** @@ -577,7 +582,7 @@ private static String toHex(byte[] bytes) { // Create the upload directory if it doesn't exist. private static File createUploadDirectory() throws NotDirectoryException { - File uploads = new File(uploadDirectory); + File uploads = getUploadDirectory(); uploads.mkdirs(); if (!uploads.isDirectory()) { throw new NotDirectoryException( diff --git a/app/playRepository/GitRepository.java b/app/playRepository/GitRepository.java index f12c5468a..9042a05bb 100644 --- a/app/playRepository/GitRepository.java +++ b/app/playRepository/GitRepository.java @@ -133,11 +133,11 @@ public static Repository buildGitRepository(String ownerName, String projectName boolean alternatesMergeRepo) { try { RepositoryBuilder repo = new RepositoryBuilder() - .setGitDir(new File(getGitDirectory(ownerName, projectName))); + .setGitDir(getGitDirectory(ownerName, projectName)); if (alternatesMergeRepo) { - repo.addAlternateObjectDirectory(new File(getDirectoryForMergingObjects(ownerName, - projectName))); + repo.addAlternateObjectDirectory(getDirectoryForMergingObjects(ownerName, + projectName)); } return repo.build(); @@ -922,7 +922,7 @@ public boolean isFile(String path) throws IOException { * @return * @see #getGitDirectory(String, String) */ - public static String getGitDirectory(Project project) { + public static File getGitDirectory(Project project) { return getGitDirectory(project.owner, project.name); } @@ -937,8 +937,7 @@ public static String getGitDirectory(Project project) { * @throws IOException */ public static String getGitDirectoryURL(Project project) throws IOException { - String currentDirectory = new java.io.File( "." ).getCanonicalPath(); - return currentDirectory + "/" + getGitDirectory(project); + return getGitDirectory(project).getCanonicalPath(); } /** @@ -951,8 +950,16 @@ public static String getGitDirectoryURL(Project project) throws IOException { * @param projectName * @return */ - public static String getGitDirectory(String ownerName, String projectName) { - return getRepoPrefix() + ownerName + "/" + projectName + ".git"; + public static File getGitDirectory(String ownerName, String projectName) { + return new File(getRootDirectory(), ownerName + "/" + projectName + ".git"); + } + + private static File getRootDirectory() { + return new File(utils.Config.getYobiHome(), getRepoPrefix()); + } + + private static File getRootDirectoryForMerging() { + return new File(utils.Config.getYobiHome(), getRepoForMergingPrefix()); } /** @@ -968,20 +975,18 @@ public static String getGitDirectory(String ownerName, String projectName) { * * @see bare repository */ public static void cloneRepository(String gitUrl, Project forkingProject) throws GitAPIException { - String directory = getGitDirectory(forkingProject); Git.cloneRepository() .setURI(gitUrl) - .setDirectory(new File(directory)) + .setDirectory(getGitDirectory(forkingProject)) .setCloneAllBranches(true) .setBare(true) .call(); } public static void cloneRepository(String gitUrl, Project forkingProject, String authId, String authPw) throws GitAPIException { - String directory = getGitDirectory(forkingProject); Git.cloneRepository() .setURI(gitUrl) - .setDirectory(new File(directory)) + .setDirectory(getGitDirectory(forkingProject)) .setCloneAllBranches(true) .setBare(true) .setCredentialsProvider(new UsernamePasswordCredentialsProvider(authId, authPw)) @@ -1011,12 +1016,12 @@ public static void checkout(Repository repository, String branchName) throws Git * @param projectName * @return */ - public static String getDirectoryForMerging(String owner, String projectName) { - return getRepoForMergingPrefix() + owner + "/" + projectName + ".git"; + public static File getDirectoryForMerging(String owner, String projectName) { + return new File(getRootDirectoryForMerging(), owner + "/" + projectName + ".git"); } - public static String getDirectoryForMergingObjects(String owner, String projectName) { - return getDirectoryForMerging(owner, projectName) + "/.git/objects"; + public static File getDirectoryForMergingObjects(String owner, String projectName) { + return new File(getDirectoryForMerging(owner, projectName), ".git/objects"); } @SuppressWarnings("unchecked") @@ -1228,12 +1233,12 @@ public static Repository buildMergingRepository(PullRequest pullRequest) { } public static Repository buildMergingRepository(Project project) { - String workingTree = GitRepository.getDirectoryForMerging(project.owner, project.name); + File workingDirectory = GitRepository.getDirectoryForMerging(project.owner, project.name); try { - File gitDir = new File(workingTree + "/.git"); + File gitDir = new File(workingDirectory + "/.git"); if(!gitDir.exists()) { - return cloneRepository(project, workingTree).getRepository(); + return cloneRepository(project, workingDirectory).getRepository(); } else { return new RepositoryBuilder().setGitDir(gitDir).build(); } @@ -1242,10 +1247,10 @@ public static Repository buildMergingRepository(Project project) { } } - private static Git cloneRepository(Project project, String workingTreePath) throws GitAPIException, IOException { + private static Git cloneRepository(Project project, File workingDirectory) throws GitAPIException, IOException { return Git.cloneRepository() .setURI(GitRepository.getGitDirectoryURL(project)) - .setDirectory(new File(workingTreePath)) + .setDirectory(workingDirectory) .call(); } @@ -1859,10 +1864,10 @@ public boolean move(String srcProjectOwner, String srcProjectName, String desrPr WindowCacheConfig config = new WindowCacheConfig(); config.install(); - File srcGitDirectory = new File(getGitDirectory(srcProjectOwner, srcProjectName)); - File destGitDirectory = new File(getGitDirectory(desrProjectOwner, destProjectName)); - File srcGitDirectoryForMerging = new File(getDirectoryForMerging(srcProjectOwner, srcProjectName)); - File destGitDirectoryForMerging = new File(getDirectoryForMerging(desrProjectOwner, destProjectName)); + File srcGitDirectory = getGitDirectory(srcProjectOwner, srcProjectName); + File destGitDirectory = getGitDirectory(desrProjectOwner, destProjectName); + File srcGitDirectoryForMerging = getDirectoryForMerging(srcProjectOwner, srcProjectName); + File destGitDirectoryForMerging = getDirectoryForMerging(desrProjectOwner, destProjectName); srcGitDirectory.setWritable(true); srcGitDirectoryForMerging.setWritable(true); diff --git a/app/playRepository/RepositoryService.java b/app/playRepository/RepositoryService.java index aa3437f71..db7b94ff1 100644 --- a/app/playRepository/RepositoryService.java +++ b/app/playRepository/RepositoryService.java @@ -36,6 +36,7 @@ import play.mvc.Http.Request; import play.mvc.Http.Response; import playRepository.hooks.*; +import utils.Config; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -148,7 +149,7 @@ public static DAVServlet createDavServlet(final String userName) throws ServletE @Override public String getInitParameter(String name) { if (name.equals("SVNParentPath")) { - return new File(SVNRepository.getRepoPrefix() + userName + "/").getAbsolutePath(); + return new File(SVNRepository.getRootDirectory(), userName + "/").getAbsolutePath(); } else { return play.Configuration.root().getString("application." + name); } diff --git a/app/playRepository/SVNRepository.java b/app/playRepository/SVNRepository.java index 3ebac74b0..f0d11940e 100644 --- a/app/playRepository/SVNRepository.java +++ b/app/playRepository/SVNRepository.java @@ -201,14 +201,12 @@ private ObjectNode fileAsJson(String path, org.tmatesoft.svn.core.io.SVNReposito @Override public void create() throws SVNException { - String svnPath = new File(SVNRepository.getRepoPrefix() + ownerName + "/" + projectName) - .getAbsolutePath(); - SVNRepositoryFactory.createLocalRepository(new File(svnPath), true, false); + SVNRepositoryFactory.createLocalRepository(getDirectory(), true, false); } @Override public void delete() throws Exception { - FileUtil.rm_rf(new File(getRepoPrefix() + ownerName + "/" + projectName)); + FileUtil.rm_rf(getDirectory()); } @Override @@ -224,7 +222,7 @@ public String getPatch(String revA, String revB) throws SVNException, Unsupporte private String getPatch(long revA, long revB) throws SVNException, UnsupportedEncodingException { // Prepare required arguments. - SVNURL svnURL = SVNURL.fromFile(new File(getRepoPrefix() + ownerName + "/" + projectName)); + SVNURL svnURL = SVNURL.fromFile(getDirectory()); // Get diffClient. SVNClientManager clientManager = SVNClientManager.newInstance(); @@ -253,7 +251,7 @@ public List getDiff(String revA, String revB) throws IOException { public List getHistory(int page, int limit, String until, String path) throws IOException, GitAPIException, SVNException { // Get the repository - SVNURL svnURL = SVNURL.fromFile(new File(repoPrefix + ownerName + "/" + projectName)); + SVNURL svnURL = SVNURL.fromFile(getDirectory()); org.tmatesoft.svn.core.io.SVNRepository repository = SVNRepositoryFactory.create(svnURL); // path to get log @@ -287,7 +285,7 @@ public List getHistory(int page, int limit, String until, String path) t public Commit getCommit(String revNumber) throws IOException, SVNException { long rev = Integer.parseInt(revNumber); String[] paths = {"/"}; - SVNURL svnURL = SVNURL.fromFile(new File(getRepoPrefix() + ownerName + "/" + projectName)); + SVNURL svnURL = SVNURL.fromFile(getDirectory()); org.tmatesoft.svn.core.io.SVNRepository repository = SVNRepositoryFactory.create(svnURL); for(Object entry : repository.log(paths, null, rev, rev, false, false)) { @@ -326,8 +324,7 @@ public ResourceType getType() { } private org.tmatesoft.svn.core.io.SVNRepository getSVNRepository() throws SVNException { - SVNURL svnURL = SVNURL.fromFile(new File(getRepoPrefix() + ownerName + "/" + - projectName)); + SVNURL svnURL = SVNURL.fromFile(getDirectory()); return SVNRepositoryFactory.create(svnURL); } @@ -376,7 +373,7 @@ public boolean isEmpty() { SVNURL svnURL; org.tmatesoft.svn.core.io.SVNRepository repository = null; try { - svnURL = SVNURL.fromFile(new File(repoPrefix + ownerName + "/" + projectName)); + svnURL = SVNURL.fromFile(getDirectory()); repository = SVNRepositoryFactory.create(svnURL); return repository.getLatestRevision() == 0; } catch (SVNException e) { @@ -389,8 +386,8 @@ public boolean isEmpty() { } public boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName) { - File src = new File(getRepoPrefix() + srcProjectOwner + "/" + srcProjectName); - File dest = new File(getRepoPrefix() + desrProjectOwner + "/" + destProjectName); + File src = new File(getRootDirectory(), srcProjectOwner + "/" + srcProjectName); + File dest = new File(getRootDirectory(), desrProjectOwner + "/" + destProjectName); src.setWritable(true); try { @@ -406,6 +403,10 @@ public boolean move(String srcProjectOwner, String srcProjectName, String desrPr @Override public File getDirectory() { - return new File(getRepoPrefix() + ownerName + "/" + projectName); + return new File(getRootDirectory(), ownerName + "/" + projectName); + } + + public static File getRootDirectory() { + return new File(Config.getYobiHome(), getRepoPrefix()); } } diff --git a/app/utils/Config.java b/app/utils/Config.java index d68126b48..0f2b2bf95 100644 --- a/app/utils/Config.java +++ b/app/utils/Config.java @@ -20,13 +20,20 @@ */ package utils; +import com.typesafe.config.ConfigFactory; import models.SiteAdmin; import org.apache.commons.lang3.ObjectUtils; import play.Configuration; import play.mvc.Http; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.*; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Enumeration; public class Config { @@ -174,10 +181,10 @@ public static String getSystemEmailAddress() { return email; } - email = (new SiteAdmin()).admin.email; + models.User admin = (new SiteAdmin()).admin; - if (email != null) { - return email; + if (admin != null && admin.email != null) { + return admin.email; } return "yobi@yobi.io"; @@ -303,4 +310,12 @@ public T getDefault() { public static T get(Key key) { return (T) Configuration.root().getObject(key.getName(), key.getDefault()); } + + public static String getYobiHome() { + return System.getProperty("yobi.home"); + } + + public static String getYobiHome(String defaultValue) { + return System.getProperty("yobi.home", defaultValue); + } } diff --git a/app/utils/Constants.java b/app/utils/Constants.java index 0b0aa22bb..497d5b4ef 100644 --- a/app/utils/Constants.java +++ b/app/utils/Constants.java @@ -28,8 +28,6 @@ public class Constants { public static final String TITLE = "title"; public static final String DESCRIPTION = "description"; - public static final String DEFAULT_LOGO_PATH = "public/uploadFiles/"; - public static final String RESOURCE_KEY_DELIM = "_"; public static final String ADMIN_LOGIN_ID = "admin"; diff --git a/app/utils/FileUtil.java b/app/utils/FileUtil.java index ce6a51544..2dbd5b807 100644 --- a/app/utils/FileUtil.java +++ b/app/utils/FileUtil.java @@ -29,6 +29,7 @@ import java.io.*; import java.nio.charset.Charset; +import java.nio.file.Path; public class FileUtil { @@ -145,4 +146,25 @@ public static String getCharset(MediaType mediaType) { return mediaType.hasParameters() ? mediaType.getParameters().get("charset") : null; } + + /** + * Checks whether the subpath is a subpath of the given path + * + * @param subpath + * @param path + * @return true if the subpath is a subpath of the given path + * @throws IOException + */ + public static boolean isSubpathOf(Path subpath, Path path) throws IOException { + return isSubpathOf(subpath, path, true); + } + + public static boolean isSubpathOf(Path subpath, Path path, boolean resolveSymlink) throws IOException { + if (resolveSymlink) { + path = path.toRealPath(); + subpath = subpath.toRealPath(); + } + + return subpath.normalize().startsWith(path.normalize()); + } } diff --git a/app/views/welcome/restart.scala.html b/app/views/welcome/restart.scala.html index 550f388b6..20f474e8c 100644 --- a/app/views/welcome/restart.scala.html +++ b/app/views/welcome/restart.scala.html @@ -18,6 +18,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **@ +@(hasFailedToUpdateSecret: Boolean = false) @import utils.TemplateHelper._ @@ -57,6 +58,9 @@

@Messages("app.restart.welcome")

@Html(Messages("app.restart.notice")) + @if(hasFailedToUpdateSecret) { + @Html(Messages("app.restart.updateSecretYourself")) + }

diff --git a/build.sbt b/build.sbt index 352ec4832..c601547b0 100644 --- a/build.sbt +++ b/build.sbt @@ -70,6 +70,19 @@ buildInfoKeys := Seq[BuildInfoKey](name, version) buildInfoPackage := "yobi" +mappings in Universal := + (mappings in Universal).value.filterNot { case (_, file) => file.startsWith("conf/") } + +NativePackagerKeys.bashScriptExtraDefines += """# Added by build.sbt + |YOBI_HOME=$(cd "$(realpath "$(dirname "$(realpath "$0")")")/.."; pwd -P) + |addJava "-Dyobi.home=$YOBI_HOME" + | + |yobi_config_file="$YOBI_HOME"/conf/application.conf + |yobi_log_config_file="$YOBI_HOME"/conf/application-logger.xml + |[ -f "$yobi_config_file" ] && addJava "-Dconfig.file=$yobi_config_file" + |[ -f "$yobi_log_config_file" ] && addJava "-Dlogger.file=$yobi_log_config_file" + |""".stripMargin + lazy val yobi = (project in file(".")) .enablePlugins(PlayScala) .enablePlugins(SbtWeb) diff --git a/conf/messages b/conf/messages index 2f6edc61e..e31959206 100644 --- a/conf/messages +++ b/conf/messages @@ -24,6 +24,7 @@ app.description = Web-based platform for collaborative software development app.name = Yobi app.restart.notice = Server needs to be restarted. +app.restart.updateSecretYourself = Please update application.secret with random text. app.restart.welcome = Welcome! app.welcome = Tada! Welcome to {0}! app.welcome.project = Project diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index af7d3d0fb..b9cc9cbe1 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -24,6 +24,7 @@ app.description = 웹기반 소프트웨어 개발 플랫폼 app.name = Yobi app.restart.notice = 서버를 재시작해야합니다. +app.restart.updateSecretYourself = application.secret을 무작위 문자열로 변경해주십시오. app.restart.welcome = 환영합니다! app.welcome = {0}를 시작합니다! app.welcome.project = 프로젝트 diff --git a/project/BuildConfig.scala b/project/BuildConfig.scala deleted file mode 100644 index 81010acfe..000000000 --- a/project/BuildConfig.scala +++ /dev/null @@ -1,45 +0,0 @@ -import sbt._ -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.Path -import java.io.IOException; - -object ApplicationBuild extends Build { - - val APPLICATION_CONF_DEFAULT = "application.conf.default" - val APPLICATION_CONF = "application.conf" - val LOG_CONF_DEFAULT = "application-logger.xml.default" - val LOG_CONF = "application-logger.xml" - val CONFIG_DIRNAME = "conf" - - def basePath = new File(System.getProperty("user.dir")).getAbsolutePath() - - def configDirPath = basePath + "/" + CONFIG_DIRNAME - - def initConfig(pathToDefaultConfig: Path, pathToConfig: Path): Unit = { - val configFile = pathToConfig.toFile() - - if (!configFile.exists()) { - try { - Files.copy(pathToDefaultConfig, pathToConfig) - } catch { - case e: IOException => throw new Exception("Failed to initialize configuration", e) - } - } else { - if (!configFile.isFile()) { - throw new Exception("Failed to initialize configuration: '" + pathToConfig + "' is a directory.") - } - } - } - - def initConfig: Unit = { - initConfig( - Paths.get(configDirPath, APPLICATION_CONF_DEFAULT), - Paths.get(configDirPath, APPLICATION_CONF)) - initConfig( - Paths.get(configDirPath, LOG_CONF_DEFAULT), - Paths.get(configDirPath, LOG_CONF)) - } - - val main = initConfig -} diff --git a/test/playRepository/GitRepositoryTest.java b/test/playRepository/GitRepositoryTest.java index 62f45d091..3f08d7372 100644 --- a/test/playRepository/GitRepositoryTest.java +++ b/test/playRepository/GitRepositoryTest.java @@ -204,8 +204,8 @@ public void cloneRepository() throws Exception { Project original = createProject(userName, projectName); Project fork = createProject("keesun", projectName); - support.Files.rm_rf(new File(GitRepository.getGitDirectory(original))); - support.Files.rm_rf(new File(GitRepository.getGitDirectory(fork))); + support.Files.rm_rf(GitRepository.getGitDirectory(original)); + support.Files.rm_rf(GitRepository.getGitDirectory(fork)); GitRepository fromRepo = new GitRepository(userName, projectName); fromRepo.create(); @@ -215,7 +215,7 @@ public void cloneRepository() throws Exception { GitRepository.cloneRepository(gitUrl, fork); // Then - File file = new File(GitRepository.getGitDirectory(fork)); + File file = GitRepository.getGitDirectory(fork); assertThat(file.exists()).isTrue(); } diff --git a/test/support/Helpers.java b/test/support/Helpers.java index e28a825d5..5150f7727 100644 --- a/test/support/Helpers.java +++ b/test/support/Helpers.java @@ -40,6 +40,7 @@ public static Map makeTestConfig() { config.put("ebean.default", "models.*"); config.put("application.secret", "foo"); config.put("application.context", "/"); + config.put("smtp.user", "yobi"); return config; }