I use CloudSync on Synology DiskStation for syncing files from a S3 location to Synology DiskStation. Considering this step is part of my backup process, I was keen on having some sort of monitoring especially for when the sync gets delayed for whatsoever reason.

Unfortunately, CloudSync offers no such way to send out any notification for missed/delayed sync schedule. Although, this, in my opinion, should be part of the application but I digress. Anyhow, what it does provide is logs that include the a timestamp with the type of action performed along with the filename.

Synology%20DiskStation%20CloudSync%20Logs

This made me think, if I can get the most recent log, I can parse the timestamp and be able to figure out if there is any delayed sync. If so, send out a notification to destination of my choice. Fortunately, opening up the Firefox DevTools and monitoring Network tab was enough the figure out the route being called for fetching logs.

With that information in hand, I started using Appwrite function with following logic,

import axios from "axios";
import { fromUnixTime, sub, isBefore } from "date-fns";

type SynologyLoginData = {
    synotoken: string,
    sid: string,
};

type LogItem = {
    action: number;
    error_code: number;
    file_name: string,
    file_type: string,
    log_level: number,
    path: string,
    session_id: number,
    time: number
};

type CloudSyncGetLogsResponse = {
    data: {
        items: LogItem[],
        total: number
    },
    success: boolean
};

export async function synologyMonitorCloudSync() {
    const loginData = await synologyLogin();
    const logItems = await getCloudSyncLogs(loginData);

    // ensure there's at-least 1 log item
    if (logItems.length > 0) {
        const firstLogData = logItems[0];
        const acceptableLastLogDate = sub(new Date(), { days: 1 }); // maximum of 1 day gap

        if (firstLogData.error_code !== 0) {
            // CloudSync has encountered error
            // TODO - do something about it
        } else if (isBefore(fromUnixTime(firstLogData.time), acceptableLastLogDate)) {
            // CloudSync has potentially missed sync
            // TODO - do something about it
        }
    } else {
        // CloudSync has returned no log items, probably new sync
        // TODO Handle if required
    }

    await synologyLogout(loginData.sid);
}

function createAxiosInstance(synotoken?: string) {
    const headers: { 'Content-Type': string, 'X-SYNO-TOKEN'?: string } = {
        'Content-Type': 'application/x-www-form-urlencoded'
    };
    if (synotoken) {
        headers["X-SYNO-TOKEN"] = synotoken;
    }

    return axios.create({
        baseURL: '<SYNOLOGY_URL>',
        headers,
    });
}

async function getCloudSyncLogs(loginData: SynologyLoginData): Promise<LogItem[]> {
    const instance = createAxiosInstance(loginData.synotoken);

    try {
        const response = await instance.post<CloudSyncGetLogsResponse>(`webapi/entry.cgi`, {
            version: 1,
            method: 'get_log',
            api: 'SYNO.CloudSync',
            _sid: loginData.sid,
            offset: 0,
            limit: 1
        });

        if (!response.data.success) {
            throw new Error(JSON.stringify(response.data));
        }

        return response.data.data.items;
    } catch (err) {
        throw err;
    }
}

async function synologyLogin(): Promise<SynologyLoginData> {
    const instance = createAxiosInstance();

    try {
        const response = await instance.post(`webapi/entry.cgi`, {
            method: 'login',
            api: 'SYNO.API.Auth',
            version: 7,
            account: 'api',
            passwd: process.env.SYNOLOGY_PASSWORD,
            enable_syno_token: 'yes',
            format: 'sid'
        });

        if (!response.data.success) {
            throw new Error(JSON.stringify(response.data));
        }

        return {
            sid: response.data.data.sid,
            synotoken: response.data.data.synotoken
        };
    } catch (err) {
        throw err;
    }
}

async function synologyLogout(sid: string): Promise<void> {
    const instance = createAxiosInstance();

    try {
        await instance.post(`webapi/entry.cgi`, {
            method: 'logout',
            api: 'SYNO.API.Auth',
            version: 7,
            _sid: sid,
        });
    } catch (err) {
        throw err;
    }
}

And just like that, I now have notifications sent out to a channel in my privately hosted Mattermost server!