Upload file from client to server filesystem using Dashboard 2.0 template

Hello all,

I'm trying to implement file upload feature on my Node-Red app. I used the flow shared by Nick O'Leary in an old GoogleForum post in this link.

The first solution that Nick shared actually works well but redirects dashboard page to a page displaying only OK. Nick also provided another solution with ajax calls to solve this issue but I could not make it work in my app. The reason that the fix didn't work for me, might have been caused because I used Dashboard 2.0.

How can I implement a file upload in Dashboard 2.0? I'm sharing the flow that worked with redirection below.

Thanks

[
    {
        "id": "61dfcbcdc7a29c9a",
        "type": "ui-template",
        "z": "c911c322aad92eb4",
        "group": "e6e8fb22f71d7392",
        "page": "",
        "ui": "",
        "name": "File Access",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n\n<form action=\"/file-upload\" method=\"post\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"fileToUpload\" id=\"fileToUpload\">\n    <input type=\"submit\" value=\"Upload Image\" name=\"submit\">\n</form>\n\n</template>\n\n\n\n<script>\n\n    \n\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                videoFacingMode: 'user', // 'user' or 'environment'\n                vConstraints: {\n                    audio: false,\n                    video: {\n                        facingMode: 'user'\n                    }\n                }\n            }\n        },\n        watch: {\n            // watch for any changes of \"count\"\n            \n        },\n        computed: {\n            // automatically compute this variable\n            // whenever VueJS deems appropriate\n            \n        },\n        methods: {\n            // expose a method to our <template> and Vue Application\n            increase: function () {\n                this.count++\n            },\n            takePhoto: function () {\n            // played the sound\n            //snap.currentTime = 0;\n            //snap.play();\n            \n            // take the data out of the canvas\n            const canvas = document.querySelector('.photo');\n            const data = canvas.toDataURL('image/jpeg');\n            const link = document.createElement('a');\n            const strip = document.querySelector('.strip');\n            const photoContainer = document.getElementById(\"photoContainer\");\n            \n\n\n            link.href = data;\n            link.setAttribute('download', 'Picture');\n            link.innerHTML = `<img src=\"${data}\" alt=\"The picture\" />`;\n                            strip.insertBefore(link, strip.firstChild);\n            //click method downloads the image\n            link.click();\n            const arr = photoContainer.innerText;\n            arr.push(data);\n            photoContainer.innerText = arr;\n            console.log(photoContainer);\n                        },\n            takePhotoAndDownload: function(){\n            // played the sound\n            //snap.currentTime = 0;\n            //snap.play();\n            \n            // take the data out of the canvas\n            const canvas = document.querySelector('.photo');\n            const data = canvas.toDataURL('image/jpeg');\n            const link = document.createElement('a');\n            const strip = document.querySelector('.strip');\n            const photoArr = document.querySelector('.photoArr');\n            \n            link.href = data;\n            link.download = \"testfilename.jpeg\";\n            //click method downloads the image\n            link.click();\n            photoArr.push(data);\n            }\n\n        },\n        mounted() {\n            // code here when the component is first loaded\n\n            const video = document.querySelector('.player');\n            const canvas = document.querySelector('.photo');\n            const ctx = canvas.getContext('2d');\n            const strip = document.querySelector('.strip');\n            const snap = document.querySelector('.snap');\n\n            const photoContainer = document.createElement(\"photoContainer\");\n            const arr = [];\n            photoContainer.innerText = arr;\n            console.log(photoContainer);\n\n            // Fix for iOS Safari from https://leemartin.dev/hello-webrtc-on-safari-11-e8bcb5335295\n            video.setAttribute('autoplay', '');\n            video.setAttribute('muted', '');\n            video.setAttribute('playsinline', '')\n\n            // const constraints = {\n            // audio: false,\n            //     video: {\n            //         facingMode: 'environment' // 'user' or 'environment'\n            //     }\n            // }\n            var facingMode = 'user';\n            this.videoFacingMode = \"user\";\n            \n\n            var constraints = {\n                audio: false,\n                video: {\n                    facingMode: this.videoFacingMode//'environment' // 'user' or 'environment'\n                }\n            }\n\n\n            video.addEventListener('click', function() {\n                if (facingMode == \"user\") {\n                    facingMode = \"environment\";\n                } else {\n                    facingMode = \"user\";\n                }\n                \n                constraints = {\n                    audio: false,\n                    video: {\n                        facingMode: facingMode\n                    }\n                }\n                \n                navigator.mediaDevices.getUserMedia(constraints).then(function success(stream) {\n                    video.srcObject = stream;\n                });\n            });\n\n\n            function getVideo() {\n                \n                navigator.mediaDevices.getUserMedia(constraints)\n                    .then(localMediaStream => {\n                    console.log(localMediaStream);\n                \n            //  DEPRECIATION : \n            //       The following has been depreceated by major browsers as of Chrome and Firefox.\n            //       video.src = window.URL.createObjectURL(localMediaStream);\n            //       Please refer to these:\n            //       Deprecated  - https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL\n            //       Newer Syntax - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject\n                console.dir(video);\n                if ('srcObject' in video) {\n                    video.srcObject = localMediaStream;\n                } else {\n                    video.src = URL.createObjectURL(localMediaStream);\n                }\n                // video.src = window.URL.createObjectURL(localMediaStream);\n                video.play();\n                })\n                .catch(err => {\n                console.error(`OH NO!!!!`, err);\n                });\n            }\n\n            function paintToCanvas() {\n                const width = video.videoWidth;\n                const height = video.videoHeight;\n                canvas.width = width;\n                canvas.height = height;\n\n                return setInterval(() => {\n                    ctx.drawImage(video, 0, 0, width, height);\n                    // take the pixels out\n                    // let pixels = ctx.getImageData(0, 0, width, height);\n                    // mess with them\n                    // pixels = redEffect(pixels);\n\n                    // pixels = rgbSplit(pixels);\n                    // ctx.globalAlpha = 0.8;\n\n                    // pixels = greenScreen(pixels);\n                    // put them back\n                    // ctx.putImageData(pixels, 0, 0);\n                }, 16);\n            }\n\n            \n\n            function redEffect(pixels) {\n                for (let i = 0; i < pixels.data.length; i+=4) {\n                    pixels.data[i + 0] = pixels.data[i + 0] + 200; // RED\n                    pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREEN\n                    pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Blue\n                }\n                return pixels;\n            }\n\n            function rgbSplit(pixels) {\n                for (let i = 0; i < pixels.data.length; i+=4) {\n                    pixels.data[i - 150] = pixels.data[i + 0]; // RED\n                    pixels.data[i + 500] = pixels.data[i + 1]; // GREEN\n                    pixels.data[i - 550] = pixels.data[i + 2]; // Blue\n                }\n                return pixels;\n            }\n\n            function greenScreen(pixels) {\n                const levels = {};\n\n                document.querySelectorAll('.rgb input').forEach((input) => {\n                    levels[input.name] = input.value;\n                });\n\n                for (i = 0; i < pixels.data.length; i = i + 4) {\n                    red = pixels.data[i + 0];\n                    green = pixels.data[i + 1];\n                    blue = pixels.data[i + 2];\n                    alpha = pixels.data[i + 3];\n\n                    if (red >= levels.rmin\n                    && green >= levels.gmin\n                    && blue >= levels.bmin\n                    && red <= levels.rmax\n                    && green <= levels.gmax\n                    && blue <= levels.bmax) {\n                    // take it out!\n                    pixels.data[i + 3] = 0;\n                    }\n                }\n\n                return pixels;\n            }\n\n            function takePhoto() {\n                // played the sound\n                //snap.currentTime = 0;\n                //snap.play();\n                \n                // take the data out of the canvas\n                const data = canvas.toDataURL('image/jpeg');\n                const link = document.createElement('a');\n                link.href = data;\n                link.setAttribute('download', 'picture');\n                link.innerHTML = `<img src=\"${data}\" alt=\"The Picture\" />`;\n                                strip.insertBefore(link, strip.firstChild);\n            }\n            \n            \n            //getVideo();\n\n            //video.addEventListener('canplay', paintToCanvas);\n        },\n        unmounted() {\n            // code here when the component is removed from the Dashboard\n            // i.e. when the user navigates away from the page\n        }\n\n        \n    }\n</script>\n<style>\n    /* define any styles here - supports raw CSS */\n    .my-class {\n        color: red;\n    }\n    html {\n    box-sizing: border-box;\n    }\n    \n    *, *:before, *:after {\n    box-sizing: inherit;\n    }\n    \n    html {\n    font-size: 10px;\n    background: #ff0000;\n    }\n    \n    .photobooth {\n    background: white;\n    max-width: 150rem;\n    margin: 2rem auto;\n    border-radius: 2px;\n    }\n    \n    /*clearfix*/\n    .photobooth:after {\n    content: '';\n    display: block;\n    clear: both;\n    }\n    \n    .photo {\n    width: 100%;\n    float: left;\n    }\n    \n    .player {\n    position: absolute;\n    top: 20px;\n    right: 20px;\n    width:200px;\n    }\n    \n    /*\n    Strip!\n    */\n    \n    .strip {\n    padding: 2rem;\n    }\n    \n    .strip img {\n    width: 100px;\n    overflow-x: scroll;\n    padding: 0.8rem 0.8rem 2.5rem 0.8rem;\n    box-shadow: 0 0 3px rgba(0,0,0,0.2);\n    background: white;\n    }\n    \n    .strip a:nth-child(5n+1) img { transform: rotate(10deg); }\n    .strip a:nth-child(5n+2) img { transform: rotate(-2deg); }\n    .strip a:nth-child(5n+3) img { transform: rotate(8deg); }\n    .strip a:nth-child(5n+4) img { transform: rotate(-11deg); }\n    .strip a:nth-child(5n+5) img { transform: rotate(12deg); }\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 290,
        "y": 860,
        "wires": [
            []
        ]
    },
    {
        "id": "90de0cfe.ca90a",
        "type": "http in",
        "z": "c911c322aad92eb4",
        "name": "",
        "url": "/file-upload",
        "method": "post",
        "upload": true,
        "swaggerDoc": "",
        "x": 314,
        "y": 932,
        "wires": [
            [
                "f44eee6a.fa1a8",
                "7835838e.a259cc"
            ]
        ]
    },
    {
        "id": "f44eee6a.fa1a8",
        "type": "debug",
        "z": "c911c322aad92eb4",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "req.files",
        "x": 630,
        "y": 940,
        "wires": []
    },
    {
        "id": "7835838e.a259cc",
        "type": "function",
        "z": "c911c322aad92eb4",
        "name": "",
        "func": "msg.payload = \"OK\";\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 421,
        "y": 984,
        "wires": [
            [
                "b2930be5.501e38"
            ]
        ]
    },
    {
        "id": "b2930be5.501e38",
        "type": "http response",
        "z": "c911c322aad92eb4",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 521,
        "y": 1024,
        "wires": []
    },
    {
        "id": "e6e8fb22f71d7392",
        "type": "ui-group",
        "name": "My Group",
        "page": "e2669f7ebdf6c137",
        "width": "6",
        "height": "1",
        "order": -1,
        "showTitle": true,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "e2669f7ebdf6c137",
        "type": "ui-page",
        "name": "Page N",
        "ui": "cf18f162c725eabc",
        "path": "/pageN",
        "icon": "home",
        "layout": "grid",
        "theme": "f33b4cc2097266ae",
        "order": -1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "cf18f162c725eabc",
        "type": "ui-base",
        "name": "My Dashboard1",
        "path": "/dashboard",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "navigationStyle": "default"
    },
    {
        "id": "f33b4cc2097266ae",
        "type": "ui-theme",
        "name": "Default Theme",
        "colors": {
            "surface": "#ffffff",
            "primary": "#0094CE",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        },
        "sizes": {
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    }
]

See this recent example (and follow its links)

1 Like

Just to update here, we now have a file-upload example included in our documentation: UI Template Examples | Node-RED Dashboard 2.0

3 Likes

Thank you Steve and Joe, I managed to implement file upload feature using your solutions.