Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File lock for svn #1847

Merged
merged 6 commits into from Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/changelog/lock_command_for_svn.yaml
@@ -0,0 +1,2 @@
- type: added
description: Lock and unlock command for SVN ([#1847](https://github.com/scm-manager/scm-manager/pull/1847))
11 changes: 11 additions & 0 deletions scm-core/src/main/java/sonia/scm/repository/api/FileLock.java
Expand Up @@ -26,6 +26,7 @@

import java.io.Serializable;
import java.time.Instant;
import java.util.Optional;

/**
* Detailes of a file lock.
Expand All @@ -39,12 +40,18 @@ public class FileLock implements Serializable {
private final String id;
private final String userId;
private final Instant timestamp;
private final String message;

public FileLock(String path, String id, String userId, Instant timestamp) {
this(path, id, userId, timestamp, null);
}

public FileLock(String path, String id, String userId, Instant timestamp, String message) {
this.path = path;
this.id = id;
this.userId = userId;
this.timestamp = timestamp;
this.message = message;
}

/**
Expand Down Expand Up @@ -74,4 +81,8 @@ public String getUserId() {
public Instant getTimestamp() {
return timestamp;
}

public Optional<String> getMessage() {
return Optional.ofNullable(message);
}
}
@@ -0,0 +1,162 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package sonia.scm.repository.spi;

import org.apache.shiro.SecurityUtils;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLock;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.io.SVNRepository;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.FileLock;
import sonia.scm.repository.api.FileLockedException;
import sonia.scm.repository.api.LockCommandResult;
import sonia.scm.repository.api.UnlockCommandResult;

import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;

import static java.util.Collections.singletonMap;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.tmatesoft.svn.core.auth.BasicAuthenticationManager.newInstance;
import static sonia.scm.ContextEntry.ContextBuilder.entity;

public class SvnFileLockCommand extends AbstractSvnCommand implements FileLockCommand {

private static final String LOCK_MESSAGE_PREFIX = "locked by SCM-Manager for ";

protected SvnFileLockCommand(SvnContext context) {
super(context);
}

@Override
public LockCommandResult lock(LockCommandRequest request) {
String fileToLock = request.getFile();
try {
doLock(fileToLock);
return new LockCommandResult(true);
} catch (SVNException e) {
throw new InternalRepositoryException(entity("File", fileToLock).in(repository), "failed to lock file", e);
}
}

private void doLock(String fileToLock) throws SVNException {
SVNRepository svnRepository = open();
String currentUser = initializeAuthentication(svnRepository);
getFileLock(fileToLock, svnRepository)
.ifPresent(lock -> {
throw new FileLockedException(repository.getNamespaceAndName(), lock);
});
svnRepository.lock(singletonMap(fileToLock, null), LOCK_MESSAGE_PREFIX + currentUser, false, null);
}

@Override
public UnlockCommandResult unlock(UnlockCommandRequest request) {
return unlock(request, any -> true);
}

public UnlockCommandResult unlock(UnlockCommandRequest request, Predicate<SvnFileLock> predicate) {
String fileToUnlock = request.getFile();
try {
doUnlock(request, predicate);
return new UnlockCommandResult(true);
} catch (SVNException e) {
throw new InternalRepositoryException(entity("File", fileToUnlock).in(repository), "failed to unlock file", e);
}
}

private void doUnlock(UnlockCommandRequest request, Predicate<SvnFileLock> predicate) throws SVNException {
String fileToUnlock = request.getFile();
SVNRepository svnRepository = open();
initializeAuthentication(svnRepository);
Optional<SvnFileLock> fileLock = getFileLock(fileToUnlock, svnRepository);
if (fileLock.isPresent()) {
SvnFileLock lock = fileLock.get();
if (!request.isForce() && !getCurrentUser().equals(lock.getUserId()) || !predicate.test(lock)) {
pfeuffer marked this conversation as resolved.
Show resolved Hide resolved
throw new FileLockedException(repository.getNamespaceAndName(), lock);
}
svnRepository.unlock(singletonMap(fileToUnlock, lock.getId()), request.isForce(), null);
}
}

@Override
public Optional<FileLock> status(LockStatusCommandRequest request) {
String file = request.getFile();
try {
return getFileLock(file, open()).map(lock -> lock);
eheimbuch marked this conversation as resolved.
Show resolved Hide resolved
} catch (SVNException e) {
throw new InternalRepositoryException(entity("File", file).in(repository), "failed to read lock status", e);
}
}

@Override
public Collection<FileLock> getAll() {
try {
SVNRepository svnRepository = open();
return Arrays.stream(svnRepository.getLocks("/"))
.map(this::createLock)
.collect(toList());
} catch (SVNException e) {
throw new InternalRepositoryException(repository, "failed to read locks", e);
}
}

private Optional<SvnFileLock> getFileLock(String file, SVNRepository svnRepository) throws SVNException {
return ofNullable(svnRepository.getLock(file)).map(this::createLock);
}

private SvnFileLock createLock(SVNLock lock) {
String path = lock.getPath();
if (path.startsWith("/")) {
path = path.substring(1);
}
return new SvnFileLock(path, lock.getID(), lock.getOwner(), lock.getCreationDate().toInstant(), lock.getComment());
}

private String initializeAuthentication(SVNRepository svnRepository) {
String currentUser = getCurrentUser();
ISVNAuthenticationManager authenticationManager = newInstance(currentUser, null);
svnRepository.setAuthenticationManager(authenticationManager);
return currentUser;
}

private String getCurrentUser() {
return SecurityUtils.getSubject().getPrincipal().toString();
}

static class SvnFileLock extends FileLock {
private SvnFileLock(String path, String id, String userId, Instant timestamp, String message) {
super(path, id, userId, timestamp, message);
}

boolean isCreatedByScmManager() {
return getMessage().filter(message -> message.startsWith(LOCK_MESSAGE_PREFIX)).isPresent();
}
}
}
Expand Up @@ -34,7 +34,6 @@
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnWorkingCopyFactory;
Expand All @@ -57,10 +56,13 @@ public class SvnModifyCommand implements ModifyCommand {
private final SvnWorkingCopyFactory workingCopyFactory;
private final Repository repository;

private final SvnFileLockCommand lockCommand;

SvnModifyCommand(SvnContext context, SvnWorkingCopyFactory workingCopyFactory) {
this.context = context;
this.repository = context.getRepository();
this.workingCopyFactory = workingCopyFactory;
this.lockCommand = new SvnFileLockCommand(context);
}

@Override
Expand Down Expand Up @@ -137,6 +139,7 @@ private ModifyWorker(SVNWCClient wcClient, File workingDirectory) {

@Override
public void doScmDelete(String toBeDeleted) {
unlock(toBeDeleted);
try {
wcClient.doDelete(new File(workingDirectory, toBeDeleted), true, true, false);
} catch (SVNException e) {
Expand All @@ -146,6 +149,7 @@ public void doScmDelete(String toBeDeleted) {

@Override
public void addFileToScm(String name, Path file) {
unlock(name);
try {
wcClient.doAdd(
file.toFile(),
Expand All @@ -161,6 +165,19 @@ public void addFileToScm(String name, Path file) {
}
}

private void unlock(String toBeDeleted) {
lockCommand.unlock(
createUnlockRequest(toBeDeleted),
SvnFileLockCommand.SvnFileLock::isCreatedByScmManager
);
}

private UnlockCommandRequest createUnlockRequest(String toBeDeleted) {
UnlockCommandRequest request = new UnlockCommandRequest();
request.setFile(toBeDeleted);
return request;
}

@Override
public File getWorkDir() {
return workingDirectory;
Expand Down
Expand Up @@ -55,7 +55,8 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
Command.MODIFY,
Command.LOOKUP,
Command.FULL_HEALTH_CHECK,
Command.MIRROR
Command.MIRROR,
Command.FILE_LOCK
);

public static final Set<Feature> FEATURES = EnumSet.of(
Expand Down Expand Up @@ -155,4 +156,9 @@ public FullHealthCheckCommand getFullHealthCheckCommand() {
public MirrorCommand getMirrorCommand() {
return new SvnMirrorCommand(context, trustManager, globalProxyConfiguration);
}

@Override
public FileLockCommand getFileLockCommand() {
return new SvnFileLockCommand(context);
}
}