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

feat: implement copy shared access operation #283

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/com/epam/aidial/core/AiDial.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ void start() throws Exception {
LockService lockService = new LockService(redis, storage.getPrefix());
resourceService = new ResourceService(vertx, redis, storage, lockService, settings("resources"), storage.getPrefix());
InvitationService invitationService = new InvitationService(resourceService, encryptionService, settings("invitations"));
ShareService shareService = new ShareService(resourceService, invitationService, encryptionService);
ShareService shareService = new ShareService(resourceService, invitationService, encryptionService, storage);
PublicationService publicationService = new PublicationService(encryptionService, resourceService, storage, generator, clock);
ResourceOperationService resourceOperationService = new ResourceOperationService(resourceService, storage, invitationService, shareService);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class ControllerSelector {
private static final Pattern PATTERN_TOKENIZE = Pattern.compile("^/+v1/deployments/([^/]+)/tokenize$");
private static final Pattern PATTERN_TRUNCATE_PROMPT = Pattern.compile("^/+v1/deployments/([^/]+)/truncate_prompt$");

private static final Pattern SHARE_RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resource/share/(create|list|discard|revoke)$");
private static final Pattern SHARE_RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resource/share/(create|list|discard|revoke|copy)$");
private static final Pattern INVITATIONS = Pattern.compile("^/v1/invitations$");
private static final Pattern INVITATION = Pattern.compile("^/v1/invitations/([a-zA-Z0-9]+)$");
private static final Pattern PUBLICATIONS = Pattern.compile("^/v1/ops/publications/(list|get|create|delete|approve|reject)$");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

import com.epam.aidial.core.Proxy;
import com.epam.aidial.core.ProxyContext;
import com.epam.aidial.core.data.CopySharedAccessRequest;
import com.epam.aidial.core.data.ListSharedResourcesRequest;
import com.epam.aidial.core.data.ResourceLinkCollection;
import com.epam.aidial.core.data.ShareResourcesRequest;
import com.epam.aidial.core.security.EncryptionService;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.LockService;
import com.epam.aidial.core.service.ResourceService;
import com.epam.aidial.core.service.ShareService;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.storage.BlobStorageUtil;
import com.epam.aidial.core.storage.ResourceDescription;
import com.epam.aidial.core.util.HttpException;
import com.epam.aidial.core.util.HttpStatus;
import com.epam.aidial.core.util.ProxyUtil;
import com.epam.aidial.core.util.ResourceUtil;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -30,6 +35,8 @@ public class ShareController {
private final EncryptionService encryptionService;
private final LockService lockService;
private final InvitationService invitationService;
private final ResourceService resourceService;
private final BlobStorage storage;

public ShareController(Proxy proxy, ProxyContext context) {
this.proxy = proxy;
Expand All @@ -38,6 +45,8 @@ public ShareController(Proxy proxy, ProxyContext context) {
this.encryptionService = proxy.getEncryptionService();
this.lockService = proxy.getLockService();
this.invitationService = proxy.getInvitationService();
this.resourceService = proxy.getResourceService();
this.storage = proxy.getStorage();
}

public Future<?> handle(Operation operation) {
Expand All @@ -46,6 +55,7 @@ public Future<?> handle(Operation operation) {
case CREATE -> createSharedResources();
case REVOKE -> revokeSharedResources();
case DISCARD -> discardSharedResources();
case COPY -> copySharedAccess();
default ->
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, "Operation %s is not supported".formatted(operation));
}
Expand Down Expand Up @@ -137,6 +147,49 @@ public Future<?> revokeSharedResources() {
.onFailure(this::handleServiceError);
}

public Future<?> copySharedAccess() {
return context.getRequest()
.body()
.compose(buffer -> {
CopySharedAccessRequest request;
try {
request = ProxyUtil.convertToObject(buffer, CopySharedAccessRequest.class);
} catch (Exception e) {
log.error("Invalid request body provided", e);
throw new IllegalArgumentException("Can't initiate copy shared access request. Incorrect body provided");
}

String sourceUrl = request.sourceUrl();
astsiapanay marked this conversation as resolved.
Show resolved Hide resolved
if (sourceUrl == null) {
throw new IllegalArgumentException("sourceUrl must be provided");
}
String destinationUrl = request.destinationUrl();
if (destinationUrl == null) {
throw new IllegalArgumentException("destinationUrl must be provided");
}

String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = encryptionService.encrypt(bucketLocation);

ResourceDescription source = ResourceDescription.fromPrivateUrl(sourceUrl, encryptionService);
if (!bucket.equals(source.getBucketName())) {
throw new IllegalArgumentException("sourceUrl does not belong to the user");
}
ResourceDescription destination = ResourceDescription.fromPrivateUrl(destinationUrl, encryptionService);
if (!bucket.equals(destination.getBucketName())) {
throw new IllegalArgumentException("destinationUrl does not belong to the user");
}

return proxy.getVertx().executeBlocking(() ->
lockService.underBucketLock(bucketLocation, () -> {
shareService.copySharedAccess(bucket, bucketLocation, source, destination);
return null;
}));
})
.onSuccess(ignore -> context.respond(HttpStatus.OK))
.onFailure(this::handleServiceError);
}

private void handleServiceError(Throwable error) {
if (error instanceof IllegalArgumentException) {
context.respond(HttpStatus.BAD_REQUEST, error.getMessage());
Expand All @@ -157,7 +210,11 @@ private ResourceLinkCollection getResourceLinkCollection(Buffer buffer, Operatio
}
}

private boolean hasResource(ResourceDescription resource) {
return ResourceUtil.hasResource(resource, resourceService, storage);
}

public enum Operation {
CREATE, LIST, DISCARD, REVOKE
CREATE, LIST, DISCARD, REVOKE, COPY
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.epam.aidial.core.data;

public record CopySharedAccessRequest(String sourceUrl, String destinationUrl) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.epam.aidial.core.data.ResourceType;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.storage.ResourceDescription;
import com.epam.aidial.core.util.ResourceUtil;
import lombok.AllArgsConstructor;

@AllArgsConstructor
Expand Down Expand Up @@ -35,30 +36,34 @@ public void moveResource(String bucket, String location, ResourceDescription sou
.formatted(sourceResourceUrl, destinationResourceUrl));
}
storage.copy(sourceResourcePath, destinationResourcePath);
storage.delete(sourceResourcePath);
}
case CONVERSATION, PROMPT -> {
boolean copied = resourceService.copyResource(source, destination, overwriteIfExists);
if (!copied) {
throw new IllegalArgumentException("Can't move resource %s to %s, because destination resource already exists"
.formatted(sourceResourceUrl, destinationResourceUrl));
}
resourceService.deleteResource(source);
}
default -> throw new IllegalArgumentException("Unsupported resource type " + resourceType);
}
// move source links to destination if any
invitationService.moveResource(bucket, location, source, destination);
// move shared access if any
shareService.moveSharedAccess(bucket, location, source, destination);

deleteResource(source);
}

private boolean hasResource(ResourceDescription resource) {
return switch (resource.getType()) {
case FILE -> storage.exists(resource.getAbsoluteFilePath());
case CONVERSATION, PROMPT -> resourceService.hasResource(resource);
return ResourceUtil.hasResource(resource, resourceService, storage);
}

private void deleteResource(ResourceDescription resource) {
switch (resource.getType()) {
case FILE -> storage.delete(resource.getAbsoluteFilePath());
case CONVERSATION, PROMPT -> resourceService.deleteResource(resource);
default -> throw new IllegalArgumentException("Unsupported resource type " + resource.getType());
};
}
}

}
21 changes: 19 additions & 2 deletions src/main/java/com/epam/aidial/core/service/ShareService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import com.epam.aidial.core.data.SharedByMeDto;
import com.epam.aidial.core.data.SharedResourcesResponse;
import com.epam.aidial.core.security.EncryptionService;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.storage.BlobStorageUtil;
import com.epam.aidial.core.storage.ResourceDescription;
import com.epam.aidial.core.util.ProxyUtil;
import com.epam.aidial.core.util.ResourceUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -36,6 +38,7 @@ public class ShareService {
private final ResourceService resourceService;
private final InvitationService invitationService;
private final EncryptionService encryptionService;
private final BlobStorage storage;

/**
* Returns a list of resources shared with user.
Expand Down Expand Up @@ -304,6 +307,14 @@ public void discardSharedAccess(String bucket, String location, ResourceLinkColl
}

public void copySharedAccess(String bucket, String location, ResourceDescription source, ResourceDescription destination) {
if (!hasResource(source)) {
throw new IllegalArgumentException("source resource %s does not exists".formatted(source.getUrl()));
}

if (!hasResource(destination)) {
throw new IllegalArgumentException("destination resource %s dos not exists".formatted(destination.getUrl()));
}

ResourceType sourceResourceType = source.getType();
ResourceDescription sharedByMeResource = getShareResource(ResourceType.SHARED_BY_ME, sourceResourceType, bucket, location);
SharedByMeDto sharedByMeDto = ProxyUtil.convertToObject(resourceService.getResource(sharedByMeResource), SharedByMeDto.class);
Expand Down Expand Up @@ -361,10 +372,12 @@ private void addSharedResource(String bucket, String location, String link, Reso
ResourceDescription sharedByMeResource = getShareResource(ResourceType.SHARED_WITH_ME, resourceType, bucket, location);
resourceService.computeResource(sharedByMeResource, state -> {
ResourceLinkCollection sharedWithMe = ProxyUtil.convertToObject(state, ResourceLinkCollection.class);
if (sharedWithMe != null) {
sharedWithMe.getResources().add(new ResourceLink(link));
if (sharedWithMe == null) {
sharedWithMe = new ResourceLinkCollection(new HashSet<>());
}

sharedWithMe.getResources().add(new ResourceLink(link));

return ProxyUtil.convertToString(sharedWithMe);
});
}
Expand All @@ -389,6 +402,10 @@ private ResourceDescription getResourceFromLink(String url) {
}
}

private boolean hasResource(ResourceDescription resource) {
return ResourceUtil.hasResource(resource, resourceService, storage);
}

private ResourceDescription getShareResource(ResourceType shareResourceType, ResourceType requestedResourceType, String bucket, String location) {
return ResourceDescription.fromDecoded(shareResourceType, bucket, location,
requestedResourceType.getGroup() + BlobStorageUtil.PATH_SEPARATOR + SHARE_RESOURCE_FILENAME);
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/epam/aidial/core/util/ResourceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.epam.aidial.core.util;

import com.epam.aidial.core.service.ResourceService;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.storage.ResourceDescription;
import lombok.experimental.UtilityClass;

@UtilityClass
public class ResourceUtil {

public static boolean hasResource(ResourceDescription resource, ResourceService resourceService, BlobStorage storage) {
return switch (resource.getType()) {
case FILE -> storage.exists(resource.getAbsoluteFilePath());
case CONVERSATION, PROMPT -> resourceService.hasResource(resource);
default -> throw new IllegalArgumentException("Unsupported resource type " + resource.getType());
};
}
}
9 changes: 9 additions & 0 deletions src/test/java/com/epam/aidial/core/ResourceBaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ public class ResourceBaseTest {
}
""";

public static final String PROMPT_BODY = """
{
"id": "prompt_id",
"name": "prompt",
"folderId": "folder",
"content": "content"
}
""";

RedisServer redis;
AiDial dial;
Path testDir;
Expand Down
Loading
Loading