4

Like the title says, I'd like to know how to update the name of an attachment from a list item. The document name.

I've figured out how to get the attachment from a list item (see code below), on the condition that it is a list item that was made by the current user and that the list item has one or more attachments, but can't seem to find a way to update the name of an attachment. What I'm trying to do is to "rename" the existing attachments in list items. So if I have a list item, and it has an attachment named "Attachment.docx", I want to rename that attachment to "John Doe Attachment.docx".

My client really wants this and it would really contribute to the list and would make it much more user-friendly (long story to explain why, but it has to do with my client wanting each and every attachment to have a unique name).

    $().SPServices({
        operation: "GetListItems",
        async: false,        
        webURL: 'https://site-name-here.com/Subsite',
        listName: 'My list name',
        CAMLViewFields: "<ViewFields >" + 
                        "<FieldRef Name='Author'></FieldRef>" +                         
                        "<FieldRef Name='Attachments'></FieldRef>" +
                        "</ViewFields>",
        completefunc: function (xData, Status) {
            $(xData.responseXML).SPFilterNode("z:row").each(function() {
                // Name of the author of the list item:
                var tempName = $(this).attr("ows_Author").substring($(this).attr("ows_Author").indexOf(";#") + 2);
                // Compare the name stored in the session to the name from the author of the list item,
                // and check if the list item has attachments:
                if ((tempName == sessionStorage.getItem("sessionItemUserName", userName)) && ($(this).attr("ows_Attachments") != null)) {
                    var id =  $(this).attr("ows_ID");
                    $().SPServices({
                        operation: "GetAttachmentCollection",
                        listName: "My list name",
                        ID: id,
                        CAMLViewFields: "<ViewFields >" + 
                                        "<FieldRef Name='Title'></FieldRef>" +
                                        "</ViewFields>",
                        completefunc: function(xData, Status) {                 
                            $(xData.responseXML).find("Attachments > Attachment").each(function(i, el) {
                                var $node = $(this);
                                var filePath = $node.text();
                                var arrString = filePath.split("/");
                                var fileName = arrString[arrString.length - 1];
                                // New file name, now starting with the name of the author:
                                var newFileName = tempName + " " + fileName;
                                // Below is commented because I haven't figured out how to do this part yet:
                                /*$().SPServices({
                                    operation: 'UpdateListItems',
                                    webURL: 'https://site-name-here.com/Subsite',
                                    listName: 'My list name/Attachments',
                                    //...
                                    completefunc: function (xData, Status) {    
                                        //...
                                    }
                                });*/
                            }); // end each
                        } // end completefunc
                    }); // end SPServices
                } // end if
            }); // end each
        } // end completefunc
    }); //end spservices

I also thought about trying to copy the existing attachments and create a new attachment based on the existing ones, and then make sure that the new attachment has a different name before saving it. But I'd much rather just find a way to rename an existing attachment.

I've been searching on this for days and I really don't want to disappoint my client.
Any help would be greatly appreciated!


-EDIT: SOLVED-

Based on the code provided in wjervis' answer and the suggestion of user1065557 to put one more expand in the request (and thus getting the attachments information in the first request), I changed my code to the following:

function changeAttachmentName() {
    var attachmentID = "",
    attachmentAuthor = "",
    statusAttachment = 0,
    statusAuthor = 0,
    authorMinified,
    itemArray = [], 
    lastCreatedByCurrentUser = [],
    rightNow = new Date();  

    var url = "/_api/Web/Lists/getByTitle('My List Name')/Items",
    qs = "?$filter=Attachments eq 1&$select=ID,Author/Title,*&$expand=Author,AttachmentFiles",
    siteUrl = "https://site-name-here.com/Subsite";
    $.ajax( {
        url : siteUrl + url + qs,
        type : 'GET',
        headers : {
            'accept' : 'application/json;odata=nometadata',
            'content-type' : 'application/json;odata=nometadata'
        },
        success : successHandler,
        fail : failHandler
    } );

    function successHandler( data ) {
        $().SPServices({
            operation: "GetListItems",
            async: false,        
            webURL: 'https://site-name-here.com/Subsite',
            listName: 'My List Name',   
            CAMLViewFields: "<ViewFields >" + 
                            "<FieldRef Name='Author'></FieldRef>" +         
                            "<FieldRef Name='Attachments'></FieldRef>" +                        
                            "<FieldRef Name='Created'></FieldRef>" +
                            "</ViewFields>",
            completefunc: function (xData, Status) {
                $(xData.responseXML).SPFilterNode("z:row").each(function() {
                    if ($(this).attr("ows_Attachments") != 0) {
                        attachmentAuthor = $(this).attr("ows_Author").substring($(this).attr("ows_Author").indexOf(";#") + 2);
                        if (attachmentAuthor == sessionStorage.getItem("sessionItemUserName")) {
                            // Array of items with attachments that were made by current user:
                            itemArray.push([$(this).attr("ows_ID"), attachmentAuthor, $(this).attr("ows_Created")]);    
                        }   
                    }
                });
            }
        });

        for (var x = 0; x < itemArray.length; x++) {            // If there are items with attachments that were made by current user
            var backThen = itemArray[x][2];                     // Save date of item in itemArray
            if (Date.parse(rightNow)>Date.parse(backThen)){     // If current date is bigger than date of item in itemArray
                if (lastCreatedByCurrentUser.length == 0) {     // If lastCreatedByCurrentUser is still empty       
                    lastCreatedByCurrentUser = itemArray[x];    // Assign last created item to lastCreatedByCurrentUser
                }
                else {                                          // If lastCreatedByCurrentUser is not empty
                    if (Date.parse(backThen)>Date.parse(lastCreatedByCurrentUser[2])){      // Check if the date from current item is bigger than the date of item in lastCreatedByCurrentUser
                        lastCreatedByCurrentUser = itemArray[x];                            // Assign new last created item to lastCreatedByCurrentUser
                    }
                }
            }
        }   

        if (lastCreatedByCurrentUser.length != 0) {             // If lastCreatedByCurrentUser is not empty, meaning if there is an item
            for ( var i = 0; i < $(data).find("feed")[0].childNodes.length; i++ ) {
                if ($(data).find("feed")[0].childNodes[i].tagName == "entry") { // (Entry can only be found in items with attachments)  
                    if ($(data).find("feed")[0].childNodes[i].childNodes[0].tagName == "id") {
                        attachmentID = $(data).find("feed")[0].childNodes[i].childNodes[0].innerHTML;
                        attachmentItemID = attachmentID.substring(attachmentID.indexOf("Items(") + 6);
                        attachmentItemID = attachmentItemID.substring(0, attachmentItemID.indexOf(")"));    // ID of the list item that holds this attachment
                        if (attachmentItemID == lastCreatedByCurrentUser[0]) {
                            authorMinified = lastCreatedByCurrentUser[1].latinise();

                            for ( var j = 0; j < $(data).find("feed")[0].childNodes[i].childNodes.length; j++ ) {
                                if ($(data).find("feed")[0].childNodes[i].childNodes[j].getAttribute("title") == "AttachmentFiles") {
                                    var oldName = $(data).find("feed")[0].childNodes[i].childNodes[j].childNodes[0].childNodes[0].lastChild.lastChild.childNodes[0].childNodes[0].innerHTML,
                                    newName = 'TWL_' +  authorMinified + '_' + '(ID' + attachmentItemID + ')' + oldName.substring(oldName.lastIndexOf("."));
                                    newName = newName.replace(/ /g, "_");
                                    var oldUrl = $(data).find("feed")[0].childNodes[i].childNodes[j].childNodes[0].childNodes[0].lastChild.lastChild.childNodes[0].childNodes[1].innerHTML,
                                    newUrl = oldUrl.replace( oldName, newName );
                                    oDataUrl = siteUrl + "/_api/web/getfilebyserverrelativeurl('" + oldUrl + "')/moveto(newurl='" + newUrl + "', flags=1)";
                                }
                            }
                            $.ajax( {
                                url : oDataUrl,
                                type : 'POST',
                                headers : {
                                    'accept' : 'application/json;odata=verbose',
                                    'content-type' : 'application/json;odata=verbose',
                                    'X-HTTP-Method' : 'PUT',
                                    'X-RequestDigest' : $( '#__REQUESTDIGEST' ).val()
                                },
                                success : function( ) { console.log( 'Attachment renamed' ); },
                                fail : failHandler
                            } );
                        }
                    }   
                }
                function failHandler( data, errCode, errMessage ) { 
                    console.log( 'Error: ' + errMessage ); 
                }               
            }                       
        }
    }
    function failHandler( data, errCode, errMessage ) { 
        console.log( 'Error: ' + errMessage ); 
    }
}

This code will run when a new item has been added to the list, and only if that new item was made by the current user and has an attachment. If there already are items in the list made by the same user that also have attachments, then it will check the dates of all items and only change the name of the attachment from the item of which the date is closest to the current date (meaning that the attachment of the latest added item by the current user will be renamed).

I also used a function to latinise the name of a user, in case it contains special characters. Attachments now get renamed to the following format: "TWL_Firstname_Lastname_(ID##)".

Thank you so much guys for your help, I couldn't have done it without you. :)

2
  • 1
    Do you have to use SPServices? If you use JSOM you can call the moveTo() function of SP.File. You just need to specify the new filename in the destination url.
    – wjervis
    Nov 14, 2014 at 18:44
  • I don't necessarily need to use SPServices, as long as I can do it in Javascript then I'm fine.
    – Magali
    Nov 17, 2014 at 13:41

4 Answers 4

3

Since you don't need to use the SPServices library, you can accomplish this via REST and the moveTo() method. The following should work:

var url = "/_api/Web/Lists/getByTitle('My List Name')/Items",
qs = "?$filter=Attachments eq 1&$select=ID,Author/Title,*&$expand=Author";

$.ajax( {
    url : siteUrl + url + qs,
    type : 'GET',
    headers : {
        'accept' : 'application/json;odata=verbose', 
        'content-type' : 'application/json;odata=verbose'
    },
    success : successHandler,
    fail : failHandler
} );

function successHandler( data ) {
    data = data.d.results;
    for ( var i = 0; i < data.length; i++ ) {
        getAttachments( data[i].ID, data[i].Author.Title );
    }
}

function getAttachments( id, author ) {
    $.ajax( {
        url : siteUrl + url + '(' + id + ')/attachmentfiles',
        type : 'GET',
        headers : {
            'accept' : 'application/json;odata=verbose',
            'content-type' : 'application/json;odata=verbose'
        },
        success : renameAttachment,
        fail : failHandler
    } );

    function renameAttachment( data ) {
        data = data.d.results;
        for ( var i = 0; i < data.length; i++ ) {
            var oldName = data[i].FileName,
                newName = author + ' ' + oldName,
                oldUrl = data[i].ServerRelativeUrl,
                newUrl = oldUrl.replace( oldName, newName ),
                oDataUrl = siteUrl + "/_api/web/getfilebyserverrelativeurl('" + oldUrl + "')/moveto(newurl='" + newUrl + "', flags=1)";
            $.ajax( {
                url : oDataUrl,
                type : 'POST',
                headers : {
                    'accept' : 'application/json;odata=verbose',
                    'content-type' : 'application/json;odata=verbose',
                    'X-HTTP-Method' : 'PUT',
                    'X-RequestDigest' : $( '#__REQUESTDIGEST' ).val()
                },
                success : function( ) { console.log( 'Attachment renamed' ); },
                fail : failHandler
            } );
        }
    }
}

function failHandler( data, errCode, errMessage ) { console.log( 'Error: ' + errMessage ); }

Edit I realized I had been using the JSON Light options for the content-type/accept properties. These have been supported in SharePoint Online since around August, but I don't know if they're supported On Prem. This likely caused the issues you got that I did not.

8
  • Thank you, I'm going to try this out immediately and will let you know today or tomorrow if it does the trick. :)
    – Magali
    Nov 17, 2014 at 15:15
  • It can probably be done better using JSOM instead of REST, since you can do batch updates with JSOM instead of multiple requests, but I haven't the time to test with JSOM right now.
    – wjervis
    Nov 17, 2014 at 15:23
  • I tried out your code and I get an error at this line: "for ( var i = 0; i < data.value.length; i++ ) {". It gives the error "Cannot read property 'length' of undefined". When I log data to the console it returns #document with a <feed> tag in it with more information inside.
    – Magali
    Nov 18, 2014 at 10:44
  • Odd, that sounds like atom format. Are all the accept/content-type set to application/json;odata=<odata type> (odata can be nometadata, minimalmetadata or verbose)?
    – wjervis
    Nov 18, 2014 at 12:54
  • I found it. It's a bit strange, but by changing "data.value.length" to "$(data).find("feed")[0].childNodes.length", the error no longer occurs. I'm still editing some things here and there so I'll give you a shout when I made progress. :)
    – Magali
    Nov 18, 2014 at 13:55
2

Improving the code suggested by @wjervis, you can change the header of ajax request to:

headers: {
         'accept': 'application/json;odata=verbose'
     }

This way it will return a Json response, that I think it is more easy to handle. You can get the length of the response by doing:

data.d.results.length

Additionally, you can put one more "expand" in the rest request and then get the attachments information in the first request:

qs = "?$filter=Attachments eq 1&$select=ID,Author/Title,*&$expand=Author,AttachmentFiles"

I do like to use the app for chrome devtools called Advanced REST client to help me testing the ajax requisitions. It is very helpful for testing with Sharepoint.

See the sample: enter image description here

1
  • Thank you for your suggestions! I added one more expand to the rest request, allowing me to get the attachment information in the first request. Changing the header gave me some errors though, so I didn't do that. The app you suggested looks really good, I'm going to install it later to double check the script, thank you!
    – Magali
    Nov 20, 2014 at 10:20
0

Each entity is uniquely identified using some parameter. In case of list its list guid and list items has an integer id. Similarly documents has document url.

From your requirement I don't see any straight forward way to change the URL of an attachment. You need to re-upload with new name and then delete the old attachment.

Another option is to use an event receiver and capture the document name while uploading and then change it at that point.

1
  • I thought of re-uploading it as well and then remove the old attachment, but I haven't figured out how to do so. An event receiver won't be an option, I'm trying to do this in Javascript, client-side only.
    – Magali
    Nov 17, 2014 at 13:42
0

You can't rename an attachment with SPServices directly because the SOAP Web Services don't offer that capability. The only solution would be to grab the attachment, upload it with the new name, and delete the old attachment.

IMO, the requirement is misguided, though, because people's attachments won't have the same name as the file they uploaded.

2
  • The thing is that each list item is only supposed to have none or one attachment, and the attachment has to be downloaded first (before creating a new list item), then filled in. But certain users need to be able to download an overview of all list items in that list, along with the attachments. And therefore my client wants each attachment to have a unique name, so that when the documents are donwloaded there is a clear distinction between each attachment, for they will all contain the name of the person who made it along with a date, making it unique.
    – Magali
    Nov 17, 2014 at 13:46
  • 1
    It sounds like you should be using a Document Library with a template attached for a Content Type instead of list attachments. Nov 17, 2014 at 17:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.