<name>services.blob.client</name>
<dependencies>
+ <dependency>
+ <groupId>org.nuxeo.ecm.core</groupId>
+ <artifactId>nuxeo-core-storage-sql-management</artifactId>
+ <version>5.5.0-HF07</version>
+ </dependency>
+
<!-- keep slf4j dependencies on the top -->
<dependency>
<groupId>org.slf4j</groupId>
//HTTP query param string for specifying a URI source to blob bits.
public static final String BLOB_URI_PARAM = "blobUri";
public static final String BLOB_CSID_PARAM = "blobCsid";
+ public static final String BLOB_PURGE_ORIGINAL = "blobPurgeOrig";
//Image blob metadata labels
public static final String IMAGE_MEASURED_PART_LABEL = "digitalImage";
import java.util.List;
import java.util.Map;
+import javax.ws.rs.core.MultivaluedMap;
+
import org.dom4j.Element;
/**
//
if (derivativeTerm != null || getContentFlag == true) {
StringBuffer mimeTypeBuffer = new StringBuffer();
- BlobOutput blobOutput = NuxeoImageUtils.getBlobOutput(ctx, repoSession, //FIXME: REM - If the blob's binary has been removed from the file system, then this call will return null. We need to at least spit out a meaningful error/warning message
+ BlobOutput blobOutput = NuxeoImageUtils.getBlobOutput(ctx, repoSession,
blobRepositoryId, derivativeTerm, getContentFlag, mimeTypeBuffer);
if (getContentFlag == true) {
- blobInput.setContentStream(blobOutput.getBlobInputStream());
+ if (blobOutput != null) {
+ blobInput.setContentStream(blobOutput.getBlobInputStream());
+ } else {
+ // If we can't find the blob's content, we'll return a "missing document" image
+ blobInput.setContentStream(NuxeoImageUtils.getResource(NuxeoImageUtils.DOCUMENT_MISSING_PLACEHOLDER_IMAGE));
+ mimeTypeBuffer.append(NuxeoImageUtils.MIME_JPEG);
+ }
}
if (derivativeTerm != null) {
blobInput.setMimeType(mimeType);
blobsCommon.setMimeType(mimeType);
} else {
- blobInput.setMimeType(blobsCommon.getMimeType());
+ blobInput.setMimeType(blobsCommon.getMimeType()); // Set the MIME type to the one in blobsCommon
}
blobsCommon.setRepositoryId(null); //hide the repository id from the GET results payload since it is private
ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
BlobInput blobInput = BlobUtil.getBlobInput(ctx); // The blobInput should have been put into the context by the Blob or Media resource
if (blobInput != null && blobInput.getBlobFile() != null) {
+ boolean purgeOriginal = false;
+ MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
+ String purgeOriginalStr = queryParams.getFirst(BlobClient.BLOB_PURGE_ORIGINAL);
+ if (purgeOriginalStr != null && purgeOriginalStr.isEmpty() == false) { // Find our if the caller wants us to purge/delete the original
+ purgeOriginal = true;
+ }
//
// If blobInput has a file then we just received a multipart/form-data file post or a URI query parameter
//
DocumentModel documentModel = wrapDoc.getWrappedObject();
- RepositoryInstance repoSession = this.getRepositorySession();
- BlobsCommon blobsCommon = NuxeoImageUtils.createBlobInRepository(ctx, repoSession, blobInput);
+ RepositoryInstance repoSession = this.getRepositorySession();
+
+ BlobsCommon blobsCommon = NuxeoImageUtils.createBlobInRepository(ctx, repoSession, blobInput, purgeOriginal);
blobInput.setBlobCsid(documentModel.getName()); //Assumption here is that the documentModel "name" field is storing a CSID
PoxPayloadIn input = ctx.getInput();
\r
import java.io.ByteArrayInputStream;\r
import java.io.File;\r
-import java.io.InputStream;\r
import java.io.StringWriter;\r
import java.lang.reflect.Method;\r
import java.util.ArrayList;\r
*/\r
package org.collectionspace.services.common;\r
\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileNotFoundException;\r
+import java.io.InputStream;\r
import java.sql.Connection;\r
import java.sql.PreparedStatement;\r
import java.sql.ResultSet;\r
\r
import org.collectionspace.services.config.service.InitHandler;\r
import org.collectionspace.services.common.authorization_mgt.AuthorizationCommon;\r
+import org.collectionspace.services.common.config.ConfigReader;\r
import org.collectionspace.services.common.config.ServicesConfigReaderImpl;\r
import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;\r
import org.collectionspace.services.common.init.AddIndices;\r
public String getServerRootDir() {\r
return serverRootDir;\r
}\r
+ \r
+ public InputStream getResourceAsStream(String resourceName) throws FileNotFoundException {\r
+ InputStream result = null;\r
+ \r
+ String resourcePath = getServerRootDir() + File.separator + ConfigReader.RESOURCES_DIR_PATH + File.separator + resourceName;\r
+ result = new FileInputStream(new File(resourcePath));\r
+ \r
+ return result;\r
+ }\r
\r
/*\r
* Save a copy of the DataSource instances that exist in our initial JNDI context. For some reason, after starting up\r
}
public String getResourcesDir(){
- return getConfigRootDir() + File.separator + "resources";
+ return getConfigRootDir() + File.separator + RESOURCES_DIR_NAME;
}
}
import java.awt.Font;\r
import java.awt.Graphics;\r
import java.awt.image.BufferedImage;\r
+import java.io.ByteArrayInputStream;\r
import java.io.File;\r
import java.io.ByteArrayOutputStream;\r
+import java.io.FileDescriptor;\r
+import java.io.FileInputStream;\r
+import java.io.FileNotFoundException;\r
import java.io.InputStream;\r
import java.io.BufferedInputStream;\r
import java.io.IOException;\r
+import java.io.Serializable;\r
import java.math.BigDecimal;\r
import java.math.BigInteger;\r
import java.util.HashMap;\r
import java.util.List;\r
import java.util.Map;\r
import java.util.Random;\r
+import java.util.Set;\r
+import java.lang.reflect.Field;\r
\r
import javax.imageio.ImageIO;\r
\r
//import org.nuxeo.common.utils.FileUtils;\r
\r
import org.nuxeo.ecm.platform.picture.api.ImageInfo;\r
+import org.nuxeo.ecm.platform.picture.api.ImagingDocumentConstants;\r
import org.nuxeo.ecm.platform.picture.api.ImagingService;\r
import org.nuxeo.ecm.platform.picture.api.PictureView;\r
\r
//import org.nuxeo.ecm.core.api.ejb.DocumentManagerBean;\r
//import org.nuxeo.ecm.core.storage.sql.RepositoryImpl;\r
//import org.nuxeo.ecm.core.storage.sql.Repository;\r
+import org.nuxeo.ecm.core.storage.sql.Binary;\r
+import org.nuxeo.ecm.core.storage.sql.BinaryManager;\r
import org.nuxeo.ecm.core.storage.sql.DefaultBinaryManager;\r
+import org.nuxeo.ecm.core.storage.sql.RepositoryImpl;\r
+import org.nuxeo.ecm.core.storage.sql.RepositoryResolver;\r
+import org.nuxeo.ecm.core.storage.sql.coremodel.SQLBlob;\r
import org.nuxeo.ecm.core.storage.sql.coremodel.SQLRepository;\r
//import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor;\r
\r
import org.nuxeo.ecm.core.api.IdRef;\r
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;\r
import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder;\r
+import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;\r
import org.nuxeo.ecm.core.api.impl.blob.FileBlob;\r
import org.nuxeo.ecm.core.api.impl.blob.StreamingBlob;\r
import org.nuxeo.ecm.core.api.impl.blob.ByteArrayBlob;\r
import org.slf4j.LoggerFactory;\r
//import org.nuxeo.ecm.core.repository.jcr.testing.RepositoryOSGITestCase;\r
\r
+import org.collectionspace.services.common.ServiceMain;\r
import org.collectionspace.services.common.blob.BlobInput;\r
import org.collectionspace.services.common.context.ServiceContext;\r
import org.collectionspace.services.common.datetime.GregorianCalendarDateTimeUtils;\r
import org.collectionspace.services.jaxb.BlobJAXBSchema;\r
import org.collectionspace.services.nuxeo.client.java.CommonList;\r
import org.collectionspace.services.nuxeo.extension.thumbnail.ThumbnailConstants;\r
+import org.collectionspace.services.nuxeo.util.NuxeoUtils;\r
import org.collectionspace.services.common.blob.BlobOutput;\r
\r
import org.collectionspace.services.config.service.ListResultField;\r
\r
-//import org.collectionspace.ecm.platform.quote.api.QuoteManager;\r
\r
-// TODO: Auto-generated Javadoc\r
/**\r
* The Class NuxeoImageUtils.\r
*/\r
public class NuxeoImageUtils {\r
+ \r
/** The Constant logger. */\r
private static final Logger logger = LoggerFactory\r
.getLogger(NuxeoImageUtils.class);\r
\r
- private static final String MIME_JPEG = "image/jpeg";\r
+ public static final String DOCUMENT_PLACEHOLDER_IMAGE = "documentImage.jpg";\r
+ public static final String DOCUMENT_MISSING_PLACEHOLDER_IMAGE = "documentImageMissing.jpg";\r
+ public static final String MIME_JPEG = "image/jpeg";\r
/*\r
* FIXME: REM - These constants should be coming from configuration and NOT\r
* hard coded.\r
// List<BlobListItem> blobListItems = result.getBlobListItem();\r
HashMap<String, Object> item = null;\r
for (Blob blob : docBlobs) {\r
- item = createBlobListItem(blob, uri);\r
- commonList.addItem(item);\r
+ if (blob != null) {\r
+ item = createBlobListItem(blob, uri);\r
+ commonList.addItem(item);\r
+ }\r
}\r
\r
return commonList;\r
}\r
return result;\r
}\r
+ \r
+ private static BinaryManager getBinaryManagerService() throws ClientException {\r
+ BinaryManager result = null;\r
+ try {\r
+ result = Framework.getService(BinaryManager.class);\r
+ } catch (Exception e) {\r
+ String msg = "Unable to get Nuxeo's BinaryManager service.";\r
+ logger.error(msg, e);\r
+ throw new ClientException("msg", e);\r
+ }\r
+ return result;\r
+ }\r
+ \r
\r
/**\r
* Creates the picture.\r
* @throws Exception \r
*/\r
public static BlobsCommon createBlobInRepository(ServiceContext ctx,\r
- RepositoryInstance repoSession, BlobInput blobInput) throws Exception {\r
+ RepositoryInstance repoSession,\r
+ BlobInput blobInput,\r
+ boolean purgeOriginal) throws Exception {\r
BlobsCommon result = null;\r
\r
try {\r
}\r
} \r
\r
- result = createBlobInRepository(repoSession, wspaceDoc, blobFile, null /*mime type*/);\r
+ result = createBlobInRepository(repoSession, wspaceDoc, purgeOriginal, blobFile, null /*mime type*/);\r
} catch (Exception e) {\r
logger.error("Could not create image blob", e);\r
throw e;\r
*/\r
static public BlobsCommon createBlobInRepository(RepositoryInstance nuxeoSession,\r
DocumentModel blobLocation,\r
- // InputStream file,\r
- File file, String mimeType) {\r
+ boolean purgeOriginal,\r
+ File file,\r
+ String mimeType) {\r
BlobsCommon result = null;\r
\r
try {\r
blobLocation.getPathAsString(), true,\r
file.getName());\r
logger.debug("Stop --> Finished calling Nuxeo to create the blob document.");\r
- \r
+\r
result = createBlobsCommon(documentModel, fileBlob); // Now create our metadata resource document\r
+\r
+ // If the sender only wanted use to generate derivatives, we need to clear the original content\r
+ if (purgeOriginal == true) {\r
+ // Empty the document model's "content" property -this does not delete the actual file/blob\r
+ documentModel.setPropertyValue("file:content", (Serializable) null);\r
+ \r
+ if (documentModel.hasFacet(ImagingDocumentConstants.PICTURE_FACET)) {\r
+ // Now with no content, the derivative listener wants to update the derivatives. So to\r
+ // prevent the listener, we remove the "Picture" facet from the document\r
+ NuxeoUtils.removeFacet(documentModel, ImagingDocumentConstants.PICTURE_FACET); // Removing this facet ensures the original derivatives are unchanged.\r
+ nuxeoSession.saveDocument(documentModel);\r
+ // Now that we've emptied the document model's content field, we can add back the Picture facet\r
+ NuxeoUtils.addFacet(documentModel, ImagingDocumentConstants.PICTURE_FACET);\r
+ }\r
+ \r
+ nuxeoSession.saveDocument(documentModel);\r
+ // Next, we need to remove the actual file from Nuxeo's data directory\r
+ DocumentBlobHolder docBlobHolder = (DocumentBlobHolder) documentModel\r
+ .getAdapter(BlobHolder.class);\r
+ boolean deleteSuccess = NuxeoUtils.deleteFileOfBlob(docBlobHolder.getBlob());\r
+ }\r
} catch (Exception e) {\r
result = null;\r
logger.error("Could not create new Nuxeo blob document.", e); //FIXME: REM - This should probably be re-throwing the exception?\r
// }\r
// }\r
// }\r
+ \r
+ public static InputStream getResource(String resourceName) {\r
+ InputStream result = null;\r
+ \r
+ try {\r
+ result = ServiceMain.getInstance().getResourceAsStream(resourceName);\r
+ } catch (FileNotFoundException e) {\r
+ logger.error("Missing Services resource: " + resourceName, e);\r
+ }\r
+ \r
+ return result;\r
+ }\r
\r
/**\r
* Gets the image.\r
if (derivativeTerm != null) {\r
docBlob = pictureBlobHolder.getBlob(derivativeTerm);\r
// Nuxeo derivatives are all JPEG\r
- outMimeType.append(docBlob.getMimeType());\r
+ outMimeType.append(MIME_JPEG); // All Nuxeo image derivatives are JPEG images.\r
} else {\r
docBlob = pictureBlobHolder.getBlob();\r
}\r
if (getContentFlag == true) {\r
InputStream remoteStream = null;\r
if (isNonImageDerivative == false) {\r
- remoteStream = docBlob.getStream();\r
+ remoteStream = docBlob.getStream(); // This will fail if the blob's file has been deleted. FileNotFoundException thrown.\r
} else {\r
- remoteStream = NuxeoImageUtils.class.getClassLoader() // for now, non-image derivatives are just placeholder document images\r
- .getResourceAsStream("documentImage.jpg");\r
+ remoteStream = getResource(DOCUMENT_PLACEHOLDER_IMAGE);\r
outMimeType.append(MIME_JPEG);\r
}\r
BufferedInputStream bufferedInputStream = new BufferedInputStream(\r
- remoteStream); // FIXME: REM - To improve performance, try\r
- // BufferedInputStream(InputStream in, int size)?\r
+ remoteStream); \r
result.setBlobInputStream(bufferedInputStream);\r
}\r
} catch (Exception e) {\r
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.File;
+import java.lang.reflect.Field;
import java.util.GregorianCalendar;
import java.util.List;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
+import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.search.api.client.querymodel.descriptor.QueryModelDescriptor;
+import org.nuxeo.ecm.core.storage.sql.Binary;
+import org.nuxeo.ecm.core.storage.sql.coremodel.SQLBlob;
import org.nuxeo.runtime.api.Framework;
//private static final String ORDER_BY_CLAUSE_REGEX = "\\w+(_\\w+)?:\\w+( ASC| DESC)?(, \\w+(_\\w+)?:\\w+( ASC| DESC)?)*";
// Allow paths so can sort on complex fields. CSPACE-4601
private static final String ORDER_BY_CLAUSE_REGEX = "\\w+(_\\w+)?:\\w+(/(\\*|\\w+))*( ASC| DESC)?(, \\w+(_\\w+)?:\\w+(/(\\*|\\w+))*( ASC| DESC)?)*";
+
+ /*
+ * Keep this method private. This method uses reflection to gain access to a protected field in Nuxeo's "Binary" class. Once we learn how
+ * to locate the "file" field of a Binary instance without breaking our "contract" with this class, we should minimize
+ * our use of this method.
+ */
+ private static File getFileOfBlob(Blob blob) {
+ File result = null;
+
+ if (blob instanceof SQLBlob) {
+ SQLBlob sqlBlob = (SQLBlob)blob;
+ Binary binary = sqlBlob.getBinary();
+ try {
+ Field fileField = binary.getClass().getDeclaredField("file");
+ boolean accessibleState = fileField.isAccessible();
+ if (accessibleState == false) {
+ fileField.setAccessible(true);
+ }
+ result = (File)fileField.get(binary);
+ fileField.setAccessible(accessibleState); // set it back to its original access state
+ } catch (Exception e) {
+ logger.error("Was not able to find the 'file' field", e);
+ }
+ }
+
+ return result;
+ }
+
+ static public boolean deleteFileOfBlob(Blob blob) {
+ boolean result = false;
+
+ File fileToDelete = getFileOfBlob(blob);
+ result = fileToDelete.delete();
+ if (result == false) {
+ logger.warn("Could not delete the blob file at: " + fileToDelete.getAbsolutePath());
+ }
+
+ return result;
+ }
+
+ /*
+ * This method will fail to return a facet list if non exist or if Nuxeo changes the
+ * DocumentModelImpl class "facets" field to be of a different type or if they remove it altogether.
+ */
+ public static Set<String> getFacets(DocumentModel docModel) {
+ Set<String> result = null;
+
+ try {
+ Field f = docModel.getClass().getDeclaredField("facets");
+ f.setAccessible(true);
+ result = (Set<String>) f.get(docModel);
+ f.setAccessible(false);
+ } catch (Exception e) {
+ logger.error("Could not remove facet from DocumentModel instance: " + docModel.getId(), e);
+ }
+
+ return result;
+ }
+
+ /*
+ * Remove a Nuxeo facet from a document model instance
+ */
+ public static boolean removeFacet(DocumentModel docModel, String facet) {
+ boolean result = false;
+
+ Set<String> facets = getFacets(docModel);
+ if (facets != null && facets.contains(facet)) {
+ facets.remove(facet);
+ result = true;
+ }
+
+ return result;
+ }
+
+ /*
+ * Adds a Nuxeo facet to a document model instance
+ */
+ public static boolean addFacet(DocumentModel docModel, String facet) {
+ boolean result = false;
+
+ Set<String> facets = getFacets(docModel);
+ if (facets != null && !facets.contains(facet)) {
+ facets.add(facet);
+ result = true;
+ }
+ return result;
+ }
public static void exportDocModel(DocumentModel src) {
DocumentReader reader = null;
protected String getAbsoluteFileName(String configFileName) {
return serverRootDir
+ File.separator + CSPACE_DIR_NAME
- + File.separator + CONFIG_DIR_NAME
+ + File.separator + CONFIG_DIR_PATH
+ File.separator + configFileName;
}
protected String getConfigRootDir() {
return serverRootDir
+ File.separator + CSPACE_DIR_NAME
- + File.separator + CONFIG_DIR_NAME;
+ + File.separator + CONFIG_DIR_PATH;
}
}
public interface ConfigReader<T> {
final public static String CSPACE_DIR_NAME = "cspace";
- final public static String CONFIG_DIR_NAME = "config" + File.separator + "services";
+ final public static String CONFIG_DIR_PATH = "config" + File.separator + "services";
+ final public static String RESOURCES_DIR_NAME = "resources";
+ final public static String RESOURCES_DIR_PATH = CSPACE_DIR_NAME + File.separator + CONFIG_DIR_PATH + File.separator + RESOURCES_DIR_NAME;
/**
* getFileName - get configuration file name
/*
* Create both a new media record
*/
- public ClientResponse<Response> createMediaAndBlobWithUri(PoxPayloadOut xmlPayload, String blobUri) {
- return getProxy().createMediaAndBlobWithUri(xmlPayload.getBytes(), blobUri);
+ public ClientResponse<Response> createMediaAndBlobWithUri(PoxPayloadOut xmlPayload, String blobUri, boolean purgeOriginal) {
+ return getProxy().createMediaAndBlobWithUri(xmlPayload.getBytes(), blobUri, purgeOriginal);
}
/**
@Produces("application/xml")
@Consumes("application/xml")
ClientResponse<Response>createMediaAndBlobWithUri(byte[] xmlPayload,
- @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri);
+ @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri,
+ @QueryParam(BlobClient.BLOB_PURGE_ORIGINAL) boolean purgeOriginal);
}
public void createMediaAndBlobWithUri(String testName) throws Exception {
MediaClient client = new MediaClient();
PoxPayloadOut multipart = createMediaInstance(createIdentifier());
- ClientResponse<Response> mediaRes = client.createMediaAndBlobWithUri(multipart, PUBLIC_URL_DECK);
+ ClientResponse<Response> mediaRes = client.createMediaAndBlobWithUri(multipart, PUBLIC_URL_DECK, true); // purge the original
String mediaCsid = null;
try {
assertStatusCode(mediaRes, testName);
mediaCsid = extractId(mediaRes);
+// allResourceIdsCreated.add(mediaCsid); // Re-enable this and also add code to delete the associated blob
} finally {
if (mediaRes != null) {
mediaRes.releaseConnection();
Response response = null;
try {
- ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(BlobClient.SERVICE_NAME, (PoxPayloadIn)null); // The blobUri argument is our payload
+ MultivaluedMap<String, String> queryParams = ui.getQueryParameters();
+ ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(BlobClient.SERVICE_NAME,
+ queryParams);
BlobInput blobInput = BlobUtil.getBlobInput(ctx); // the blob doc handler will look for this in the context
- blobInput.createBlobFile(blobUri);
+ blobInput.createBlobFile(blobUri); // The blobUri argument is our payload
response = this.create((PoxPayloadIn)null, ctx); // By now the binary bits have been created and we just need to create the metadata blob record -this info is in the blobInput var
} catch (Exception e) {
throw bigReThrow(e, ServiceMessages.CREATE_FAILED);
}
/*
- * Looks for a blobUri query param from a POST. If it finds one then it creates a blob and a media resource and associates them.
+ * Looks for a blobUri query param from a POST. If it finds one then it creates a blob AND a media resource and associates them.
* (non-Javadoc)
* @see org.collectionspace.services.common.ResourceBase#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.ResourceMap, javax.ws.rs.core.UriInfo, java.lang.String)
*/
MultivaluedMap<String, String> queryParams = ui.getQueryParameters();
String blobUri = queryParams.getFirst(BlobClient.BLOB_URI_PARAM);
if (blobUri != null && blobUri.isEmpty() == false) {
- result = createBlobWithUri(resourceMap, ui, xmlPayload, blobUri);
+ result = createBlobWithUri(resourceMap, ui, xmlPayload, blobUri); // uses the blob resource and doc handler to create the blob
String blobCsid = CollectionSpaceClientUtils.extractId(result);
queryParams.add(BlobClient.BLOB_CSID_PARAM, blobCsid); // Add the new blob's csid as an artificial query param -the media doc handler will look for this
}
ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(BlobClient.SERVICE_NAME, input);
BlobInput blobInput = BlobUtil.getBlobInput(ctx);
blobInput.createBlobFile(blobUri);
- response = this.create(input, ctx);
+ response = this.create(input, ctx); // calls the blob resource/doc-handler to create the blob
//
// Next, update the Media record to be linked to the blob
//