Skip to content

Add lastModified date to BoxFolder#uploadFile method for com.box:box-java-sdk library. #1576

@isabsent

Description

@isabsent

I am using com.box:box-java-sdk:5.0.0. The BoxFolder class has a method called
BoxFile.Info uploadFile(InputStream fileContent, String name, long fileSize, ProgressListener listener)
which doesn't provide the ability to set the time of file creation. This means that when uploading a file from another file system via InputStream that was created a long time ago, the file that appears in the cloud is a newly created file. This is incorrect and confuses users. You need to add a method with the signature
BoxFile.Info uploadFile(InputStream fileContent, String name, Date lastModified, long fileSize, ProgressListener listener).

Moreover, your API has a similar method called
BoxFile.Info uploadNewVersion(InputStream fileContent, Date modified, long fileSize, ProgressListener listener), but it only applies to updating an existing file, not uploading a new one.

Here's some sample code you could add to your library com.box:box-java-sdk:5.0.0. to support the user setting the date when uploading a file via a InputStream:

private static final String
        BOX_UPLOAD_URL = "https://upload.box.com/api/2.0/files/content",
        CREATE_SESSION_URL = "https://upload.box.com/api/2.0/files/upload_sessions",
        UPLOAD_SESSION_URL_TEMPLATE = "https://upload.box.com/api/2.0/files/upload_sessions/%s",
        COMMIT_SESSION_URL_TEMPLATE = "https://upload.box.com/api/2.0/files/upload_sessions/%s/commit";

private static final MediaType
        JSON = MediaType.get("application/json; charset=utf-8"),
        OCTET = MediaType.get("application/octet-stream");

private static final String[] REQUESTED_FIELDS = {
        "name",
        "size",
        "content_modified_at",
        "parent",
        "item_status",
        "trashed_at"
};

public com.box.sdk.BoxFile.Info uploadChunked(InputStream is, String fileName, long totalSize, Date lastModified, String parentFolderId, String accessToken, BoxAPIConnection api, IProgressListener progressListener) throws Exception {
    if (totalSize <= 0)
        throw new IllegalArgumentException("totalSize must be known and >0 for chunked upload");

    OkHttpClient client = new OkHttpClient();

    JSONObject createPayload = new JSONObject();
    createPayload.put("folder_id", parentFolderId);
    createPayload.put("file_name", fileName);
    createPayload.put("file_size", totalSize);

    Request createReq = new Request.Builder()
            .url(CREATE_SESSION_URL)
            .addHeader("Authorization", "Bearer " + accessToken)
            .post(RequestBody.create(createPayload.toString(), JSON))
            .build();

    try (Response resp = client.newCall(createReq).execute()) {
        if (!resp.isSuccessful()) {
            String body = resp.body() != null ? resp.body().string() : null;
            throw new IOException("Failed to create upload session: " + resp.code() + " body=" + body);
        }
        String respBody = resp.body().string();
        JSONObject sessionJson = new JSONObject(respBody);
        String sessionId = sessionJson.getString("id");
        long partSize = sessionJson.optLong("part_size", 8 * 1024 * 1024); // fallback 8MB
        return uploadPartsAndCommit(is, fileName, totalSize, lastModified, accessToken, progressListener, sessionId, partSize, client, api);
    }
}

private com.box.sdk.BoxFile.Info uploadPartsAndCommit(InputStream is, String fileName, long totalSize, Date lastModified, String accessToken, IProgressListener progressListener, String sessionId, long partSize, OkHttpClient client, BoxAPIConnection api) throws Exception {
    List<JSONObject> uploadedParts = new ArrayList<>();
    MessageDigest wholeDigest = MessageDigest.getInstance("SHA-1");

    byte[] buffer = new byte[(int) partSize];
    long offset = 0;
    int read;
    long uploaded = 0;

    while (offset < totalSize) {
        int toRead = (int) Math.min(partSize, totalSize - offset);
        int actuallyRead = 0;
        int pos = 0;
        while (pos < toRead && (read = is.read(buffer, pos, toRead - pos)) != -1) {
            pos += read;
        }
        actuallyRead = pos;
        if (actuallyRead <= 0) {
            throw new IOException("Unexpected EOF: expected " + toRead + " but read " + actuallyRead);
        }

        wholeDigest.update(buffer, 0, actuallyRead);

        MessageDigest partDigest = MessageDigest.getInstance("SHA-1");
        partDigest.update(buffer, 0, actuallyRead);
        String partShaBase64 = Base64.encodeToString(partDigest.digest(), Base64.NO_WRAP);

        long start = offset;
        long end = offset + actuallyRead - 1;

        int finalActuallyRead = actuallyRead;
        RequestBody partBody = new RequestBody() {
            @Override
            public MediaType contentType() {
                return OCTET;
            }

            @Override
            public long contentLength() {
                return finalActuallyRead;
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                sink.write(buffer, 0, finalActuallyRead);
            }
        };

        Request uploadPartReq = new Request.Builder()
                .url(String.format(UPLOAD_SESSION_URL_TEMPLATE, sessionId))
                .addHeader("Authorization", "Bearer " + accessToken)
                .addHeader("Content-Type", "application/octet-stream")
                .addHeader("Content-Range", "bytes " + start + "-" + end + "/" + totalSize)
                .addHeader("Digest", "SHA=" + partShaBase64)
                .put(partBody)
                .build();

        try (Response uploadResp = client.newCall(uploadPartReq).execute()) {
            if (!uploadResp.isSuccessful()) {
                String b = uploadResp.body() != null ? uploadResp.body().string() : null;
                throw new IOException("Part upload failed: code=" + uploadResp.code() + " body=" + b);
            }
            String partRespBody = uploadResp.body() != null ? uploadResp.body().string() : null;
            if (partRespBody == null)
                throw new IOException("Empty part response from server");

            JSONObject partRespJson = new JSONObject(partRespBody);
            JSONObject partObj = partRespJson.optJSONObject("part");
            if (partObj == null)
                partObj = partRespJson;
            uploadedParts.add(partObj);
        }

        offset += actuallyRead;
        uploaded += actuallyRead;

        if (progressListener != null)
            progressListener.onProgress(uploaded, totalSize);
    }

    String wholeBase64 = Base64.encodeToString(wholeDigest.digest(), Base64.NO_WRAP);

    DateTimeFormatter ISO_8601_NO_MILLIS_UTC_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("UTC"));
    String isoModified = ISO_8601_NO_MILLIS_UTC_FORMATTER.format(lastModified.toInstant());

    JSONObject commitPayload = new JSONObject();
    JSONArray partsArray = new JSONArray();
    for (JSONObject p : uploadedParts)
        partsArray.put(p);

    commitPayload.put("parts", partsArray);

    JSONObject attributes = new JSONObject();
    attributes.put("content_modified_at", isoModified);
    commitPayload.put("attributes", attributes);

    RequestBody commitBody = RequestBody.create(commitPayload.toString(), JSON);

    Request commitReq = new Request.Builder()
            .url(String.format(COMMIT_SESSION_URL_TEMPLATE, sessionId))
            .addHeader("Authorization", "Bearer " + accessToken)
            .addHeader("Digest", "SHA=" + wholeBase64)
            .post(commitBody)
            .build();

    try (Response commitResp = client.newCall(commitReq).execute()) {
        String commitRespBody = commitResp.body() != null ? commitResp.body().string() : null;
        if (!commitResp.isSuccessful())
            throw new IOException("Commit failed: code=" + commitResp.code() + " body=" + commitRespBody);

        FinalResp finalResp = new Gson().fromJson(commitRespBody, FinalResp.class);
        if (!finalResp.entries.isEmpty()) {
            BoxFileInfo boxFileInfo = finalResp.entries.get(0);
            com.box.sdk.BoxFile file = new com.box.sdk.BoxFile(api, boxFileInfo.id);
            return file.getInfo(REQUESTED_FIELDS);
        }
        return null;
    }
}

public class ApiResponse {
    @SerializedName("total_count")
    public int totalCount;
    @SerializedName("entries")
    public List<Entry> entries;
}

public class BoxFileInfo {
    public String id;
    public String name;
    public long size;
    public String content_modified_at;
    public String created_at;
    public String modified_at;
}

public class BoxFileInfo {
    public String id;
    public String name;
    public long size;
    public String content_modified_at;
    public String created_at;
    public String modified_at;
}

public class Entry {
    @SerializedName("type")
    public String type;
    @SerializedName("id")
    public String id;
    @SerializedName("file_version")
    public FileVersion fileVersion;
    @SerializedName("sequence_id")
    public String sequenceId;
    @SerializedName("etag")
    public String etag;
    @SerializedName("sha1")
    public String sha1;
    @SerializedName("name")
    public String name;
    @SerializedName("description")
    public String description;
    @SerializedName("size")
    public long size;
    @SerializedName("path_collection")
    public PathCollection pathCollection;
    @SerializedName("created_at")
    public String createdAt;
    @SerializedName("modified_at")
    public String modifiedAt;
    @Nullable
    @SerializedName("trashed_at")
    public String trashedAt; // Может быть null
    @Nullable
    @SerializedName("purged_at")
    public String purgedAt; // Может быть null
    @SerializedName("content_created_at")
    public String contentCreatedAt;
    @SerializedName("content_modified_at")
    public String contentModifiedAt;
    @SerializedName("created_by")
    public User createdBy;
    @SerializedName("modified_by")
    public User modifiedBy;
    @SerializedName("owned_by")
    public User ownedBy;
    @Nullable
    @SerializedName("shared_link")
    public String sharedLink; // Может быть null
    @SerializedName("parent")
    public Parent parent;
    @SerializedName("item_status")
    public String itemStatus;
}

public class FinalResp {
    public List<BoxFileInfo> entries;
}

Metadata

Metadata

Labels

enhancementAdded to issues that describes enhancements

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions