// // ZoneMinder Event Class Implementation // Copyright (C) 2001-2008 Philip Coombes // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // #include "zm_event.h" #include "zm_camera.h" #include "zm_db.h" #include "zm_frame.h" #include "zm_logger.h" #include "zm_monitor.h" #include "zm_signal.h" #include "zm_videostore.h" #include #include #include #include //#define USE_PREPARED_SQL 1 const char * Event::frame_type_names[3] = { "Normal", "Bulk", "Alarm" }; #define MAX_DB_FRAMES 100 int Event::pre_alarm_count = 0; Event::PreAlarmData Event::pre_alarm_data[MAX_PRE_ALARM_FRAMES] = {}; Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap ) : id(0), monitor(p_monitor), start_time(p_start_time), end_time({0,0}), cause(p_cause), noteSetMap(p_noteSetMap), frames(0), alarm_frames(0), alarm_frame_written(false), tot_score(0), max_score(0), //path(""), //snapshit_file(), //alarm_file(""), videoStore(nullptr), //video_name(""), //video_file(""), last_db_frame(0), have_video_keyframe(false), //scheme save_jpegs(0) { std::string notes; createNotes(notes); timeval now = {}; gettimeofday(&now, nullptr); if ( !start_time.tv_sec ) { Warning("Event has zero time, setting to now"); start_time = now; } else if ( start_time.tv_sec > now.tv_sec ) { char buffer[26]; char buffer_now[26]; tm tm_info = {}; localtime_r(&start_time.tv_sec, &tm_info); strftime(buffer, 26, "%Y:%m:%d %H:%M:%S", &tm_info); localtime_r(&now.tv_sec, &tm_info); strftime(buffer_now, 26, "%Y:%m:%d %H:%M:%S", &tm_info); Error( "StartDateTime in the future starttime %ld.%06ld >? now %ld.%06ld difference %d\nstarttime: %s\nnow: %s", start_time.tv_sec, start_time.tv_usec, now.tv_sec, now.tv_usec, (now.tv_sec-start_time.tv_sec), buffer, buffer_now ); start_time = now; } unsigned int state_id = 0; { zmDbRow dbrow; if (dbrow.fetch("SELECT Id FROM States WHERE IsActive=1")) { state_id = atoi(dbrow[0]); } } // Copy it in case opening the mp4 doesn't work we can set it to another value save_jpegs = monitor->GetOptSaveJPEGs(); Storage * storage = monitor->getStorage(); std::string sql = stringtf( "INSERT INTO `Events` " "( `MonitorId`, `StorageId`, `Name`, `StartDateTime`, `Width`, `Height`, `Cause`, `Notes`, `StateId`, `Orientation`, `Videoed`, `DefaultVideo`, `SaveJPEGs`, `Scheme` )" " VALUES " "( %d, %d, 'New Event', from_unixtime( %ld ), %d, %d, '%s', '%s', %d, %d, %d, '%s', %d, '%s' )", monitor->Id(), storage->Id(), start_time.tv_sec, monitor->Width(), monitor->Height(), cause.c_str(), notes.c_str(), state_id, monitor->getOrientation(), 0, "", save_jpegs, storage->SchemeString().c_str() ); id = zmDbDoInsert(sql.c_str()); if ( !SetPath(storage) ) { // Try another Warning("Failed creating event dir at %s", storage->Path()); sql = stringtf("SELECT `Id` FROM `Storage` WHERE `Id` != %u", storage->Id()); if ( monitor->ServerId() ) sql += stringtf(" AND ServerId=%u", monitor->ServerId()); Debug(1, "%s", sql.c_str()); storage = nullptr; MYSQL_RES *result = zmDbFetch(sql.c_str()); if ( result ) { for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { storage = new Storage(atoi(dbrow[0])); if ( SetPath(storage) ) break; delete storage; storage = nullptr; } // end foreach row of Storage mysql_free_result(result); result = nullptr; } if ( !storage ) { Info("No valid local storage area found. Trying all other areas."); // Try remote sql = "SELECT `Id` FROM `Storage` WHERE ServerId IS NULL"; if ( monitor->ServerId() ) sql += stringtf(" OR ServerId != %u", monitor->ServerId()); result = zmDbFetch(sql.c_str()); if ( result ) { for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { storage = new Storage(atoi(dbrow[0])); if ( SetPath(storage) ) break; delete storage; storage = nullptr; } // end foreach row of Storage mysql_free_result(result); result = nullptr; } } if ( !storage ) { storage = new Storage(); Warning("Failed to find a storage area to save events."); } sql = stringtf("UPDATE Events SET StorageId = '%d' WHERE Id=%" PRIu64, storage->Id(), id); zmDbDo(sql.c_str()); } // end if ! setPath(Storage) Debug(1, "Using storage area at %s", path.c_str()); video_name = ""; snapshot_file = path + "/snapshot.jpg"; alarm_file = path + "/alarm.jpg"; /* Save as video */ if ( monitor->GetOptVideoWriter() != 0 ) { std::string container = monitor->OutputContainer(); if ( container == "auto" || container == "" ) { container = "mp4"; } video_name = stringtf("%" PRIu64 "-%s.%s", id, "video", container.c_str()); video_file = path + "/" + video_name; Debug(1, "Writing video file to %s", video_file.c_str()); videoStore = new VideoStore( video_file.c_str(), container.c_str(), monitor->GetVideoStream(), monitor->GetVideoCodecContext(), ( monitor->RecordAudio() ? monitor->GetAudioStream() : nullptr ), ( monitor->RecordAudio() ? monitor->GetAudioCodecContext() : nullptr ), monitor ); if ( !videoStore->open() ) { Warning("Failed to open videostore, turning on jpegs"); delete videoStore; videoStore = nullptr; if ( ! ( save_jpegs & 1 ) ) { save_jpegs |= 1; // Turn on jpeg storage sql = stringtf("UPDATE Events SET SaveJpegs=%d WHERE Id=%" PRIu64, save_jpegs, id); zmDbDo(sql.c_str()); } } else { sql = stringtf("UPDATE Events SET Videoed=1, DefaultVideo = '%s' WHERE Id=%" PRIu64, video_name.c_str(), id); zmDbDo(sql.c_str()); } } // end if GetOptVideoWriter } // Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap, bool p_videoEvent ) Event::~Event() { // We close the videowriter first, because if we finish the event, we might try to view the file, but we aren't done writing it yet. /* Close the video file */ if ( videoStore != nullptr ) { Debug(4, "Deleting video store"); delete videoStore; videoStore = nullptr; } // endtime is set in AddFrame, so SHOULD be set to the value of the last frame timestamp. if ( !end_time.tv_sec ) { Warning("Empty endtime for event. Should not happen. Setting to now."); gettimeofday(&end_time, nullptr); } struct DeltaTimeval delta_time; DELTA_TIMEVAL(delta_time, end_time, start_time, DT_PREC_2); Debug(2, "start_time:%d.%d end_time%d.%d", start_time.tv_sec, start_time.tv_usec, end_time.tv_sec, end_time.tv_usec); if (frame_data.size()) WriteDbFrames(); // Should not be static because we might be multi-threaded char sql[ZM_SQL_LGE_BUFSIZ]; snprintf(sql, sizeof(sql), "UPDATE Events SET Name='%s%" PRIu64 "', EndDateTime = from_unixtime(%ld), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64 " AND Name='New Event'", monitor->EventPrefix(), id, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, id); if (!zmDbDoUpdate(sql)) { // Name might have been changed during recording, so just do the update without changing the name. snprintf(sql, sizeof(sql), "UPDATE Events SET EndDateTime = from_unixtime(%ld), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, id); zmDbDoUpdate(sql); } // end if no changed rows due to Name change during recording } // Event::~Event() void Event::createNotes(std::string ¬es) { notes.clear(); for ( StringSetMap::const_iterator mapIter = noteSetMap.begin(); mapIter != noteSetMap.end(); ++mapIter ) { notes += mapIter->first; notes += ": "; const StringSet &stringSet = mapIter->second; for ( StringSet::const_iterator setIter = stringSet.begin(); setIter != stringSet.end(); ++setIter ) { if ( setIter != stringSet.begin() ) notes += ", "; notes += *setIter; } } } // void Event::createNotes(std::string ¬es) bool Event::WriteFrameImage( Image *image, struct timeval timestamp, const char *event_file, bool alarm_frame) const { int thisquality = (alarm_frame && (config.jpeg_alarm_file_quality > config.jpeg_file_quality)) ? config.jpeg_alarm_file_quality : 0; // quality to use, zero is default bool rc; if ( !config.timestamp_on_capture ) { // stash the image we plan to use in another pointer regardless if timestamped. // exif is only timestamp at present this switches on or off for write Image *ts_image = new Image(*image); monitor->TimestampImage(ts_image, ×tamp); rc = ts_image->WriteJpeg(event_file, thisquality, (monitor->Exif() ? timestamp : (timeval){0,0})); delete(ts_image); } else { rc = image->WriteJpeg(event_file, thisquality, (monitor->Exif() ? timestamp : (timeval){0,0})); } return rc; } // end Event::WriteFrameImage( Image *image, struct timeval timestamp, const char *event_file, bool alarm_frame ) bool Event::WritePacket(ZMPacket &packet) { if ( videoStore->writePacket(&packet) < 0 ) return false; return true; } // bool Event::WriteFrameVideo void Event::updateNotes(const StringSetMap &newNoteSetMap) { bool update = false; //Info( "Checking notes, %d <> %d", noteSetMap.size(), newNoteSetMap.size() ); if ( newNoteSetMap.size() > 0 ) { if ( noteSetMap.size() == 0 ) { noteSetMap = newNoteSetMap; update = true; } else { for ( StringSetMap::const_iterator newNoteSetMapIter = newNoteSetMap.begin(); newNoteSetMapIter != newNoteSetMap.end(); ++newNoteSetMapIter ) { const std::string &newNoteGroup = newNoteSetMapIter->first; const StringSet &newNoteSet = newNoteSetMapIter->second; //Info( "Got %d new strings", newNoteSet.size() ); if ( newNoteSet.size() > 0 ) { StringSetMap::iterator noteSetMapIter = noteSetMap.find(newNoteGroup); if ( noteSetMapIter == noteSetMap.end() ) { //Info( "Can't find note group %s, copying %d strings", newNoteGroup.c_str(), newNoteSet.size() ); noteSetMap.insert(StringSetMap::value_type(newNoteGroup, newNoteSet)); update = true; } else { StringSet ¬eSet = noteSetMapIter->second; //Info( "Found note group %s, got %d strings", newNoteGroup.c_str(), newNoteSet.size() ); for ( StringSet::const_iterator newNoteSetIter = newNoteSet.begin(); newNoteSetIter != newNoteSet.end(); ++newNoteSetIter ) { const std::string &newNote = *newNoteSetIter; StringSet::iterator noteSetIter = noteSet.find(newNote); if ( noteSetIter == noteSet.end() ) { noteSet.insert(newNote); update = true; } } // end for } // end if ( noteSetMap.size() == 0 } // end if newNoteSetupMap.size() > 0 } // end foreach newNoteSetMap } // end if have old notes } // end if have new notes if ( update ) { std::string notes; createNotes(notes); Debug(2, "Updating notes for event %d, '%s'", id, notes.c_str()); #if USE_PREPARED_SQL static MYSQL_STMT *stmt = 0; char notesStr[ZM_SQL_MED_BUFSIZ] = ""; unsigned long notesLen = 0; if ( !stmt ) { const char *sql = "UPDATE `Events` SET `Notes` = ? WHERE `Id` = ?"; stmt = mysql_stmt_init(&dbconn); if ( mysql_stmt_prepare(stmt, sql, strlen(sql)) ) { Fatal("Unable to prepare sql '%s': %s", sql, mysql_stmt_error(stmt)); } /* Get the parameter count from the statement */ if ( mysql_stmt_param_count(stmt) != 2 ) { Error("Unexpected parameter count %ld in sql '%s'", mysql_stmt_param_count(stmt), sql); } MYSQL_BIND bind[2]; memset(bind, 0, sizeof(bind)); /* STRING PARAM */ bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = (char *)notesStr; bind[0].buffer_length = sizeof(notesStr); bind[0].is_null = 0; bind[0].length = ¬esLen; bind[1].buffer_type= MYSQL_TYPE_LONG; bind[1].buffer= (char *)&id; bind[1].is_null= 0; bind[1].length= 0; /* Bind the buffers */ if ( mysql_stmt_bind_param(stmt, bind) ) { Error("Unable to bind sql '%s': %s", sql, mysql_stmt_error(stmt)); } } // end if ! stmt strncpy(notesStr, notes.c_str(), sizeof(notesStr)); if ( mysql_stmt_execute(stmt) ) { Error("Unable to execute sql '%s': %s", sql, mysql_stmt_error(stmt)); } #else char sql[ZM_SQL_LGE_BUFSIZ]; static char escapedNotes[ZM_SQL_MED_BUFSIZ]; mysql_real_escape_string(&dbconn, escapedNotes, notes.c_str(), notes.length()); snprintf(sql, sizeof(sql), "UPDATE `Events` SET `Notes` = '%s' WHERE `Id` = %" PRIu64, escapedNotes, id); dbQueue.push(std::move(sql)); #endif } // end if update } // void Event::updateNotes(const StringSetMap &newNoteSetMap) void Event::AddPacket(ZMPacket *packet) { have_video_keyframe = have_video_keyframe || ( ( packet->codec_type == AVMEDIA_TYPE_VIDEO ) && ( packet->keyframe || monitor->GetOptVideoWriter() == Monitor::ENCODE) ); Debug(2, "have_video_keyframe %d codec_type %d == video? %d packet keyframe %d", have_video_keyframe, packet->codec_type, (packet->codec_type == AVMEDIA_TYPE_VIDEO), packet->keyframe); ZM_DUMP_PACKET(packet->packet, "Adding to event"); if (videoStore) { if (have_video_keyframe) { videoStore->writePacket(packet); } else { Debug(2, "No video keyframe yet, not writing"); } //FIXME if it fails, we should write a jpeg } if ((packet->codec_type == AVMEDIA_TYPE_VIDEO) or packet->image) AddFrame(packet->image, *(packet->timestamp), packet->zone_stats, packet->score, packet->analysis_image); end_time = *packet->timestamp; return; } void Event::WriteDbFrames() { std::string frame_insert_sql = "INSERT INTO `Frames` (`EventId`, `FrameId`, `Type`, `TimeStamp`, `Delta`, `Score`) VALUES "; std::string stats_insert_sql = "INSERT INTO `Stats` (`EventId`, `FrameId`, `MonitorId`, `ZoneId`, " "`PixelDiff`, `AlarmPixels`, `FilterPixels`, `BlobPixels`," "`Blobs`,`MinBlobSize`, `MaxBlobSize`, " "`MinX`, `MinY`, `MaxX`, `MaxY`,`Score`) VALUES "; Debug(1, "Inserting %d frames", frame_data.size()); while (frame_data.size()) { Frame *frame = frame_data.front(); frame_data.pop(); frame_insert_sql += stringtf("\n( %" PRIu64 ", %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d ),", id, frame->frame_id, frame_type_names[frame->type], frame->timestamp.tv_sec, frame->delta.positive ? "" : "-", frame->delta.sec, frame->delta.fsec, frame->score); if (config.record_event_stats and frame->zone_stats.size()) { Debug(1, "ZOne stats size for frame %d: %d", frame->frame_id, frame->zone_stats.size()); for (ZoneStats &stats : frame->zone_stats) { stats_insert_sql += stringtf("\n(%" PRIu64 ",%d,%u,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%u),", id, frame->frame_id, monitor->Id(), stats.zone_id_, stats.pixel_diff_, stats.alarm_pixels_, stats.alarm_filter_pixels_, stats.alarm_blob_pixels_, stats.alarm_blobs_, stats.min_blob_size_, stats.max_blob_size_, stats.alarm_box_.LoX(), stats.alarm_box_.LoY(), stats.alarm_box_.HiX(), stats.alarm_box_.HiY(), stats.score_); } // end foreach zone stats } // end if recording stats delete frame; } // end while frames // The -1 is for the extra , added for values above frame_insert_sql.erase(frame_insert_sql.size()-1); //zmDbDo(frame_insert_sql.c_str()); dbQueue.push(std::move(frame_insert_sql)); if (stats_insert_sql.size() > 208) { // The -1 is for the extra , added for values above stats_insert_sql.erase(stats_insert_sql.size()-1); //zmDbDo(stats_insert_sql); dbQueue.push(std::move(stats_insert_sql)); } } // end void Event::WriteDbFrames() void Event::AddFrame( Image *image, struct timeval timestamp, const std::list &zone_stats, int score, Image *alarm_image) { if (!timestamp.tv_sec) { Warning("Not adding new frame, zero timestamp"); return; } frames++; Monitor::State monitor_state = monitor->GetState(); bool write_to_db = false; FrameType frame_type = ( ( score > 0 ) ? ALARM : ( ( ( monitor_state == Monitor::TAPE ) and ( config.bulk_frame_interval > 1 ) and ( ! (frames % config.bulk_frame_interval) ) ) ? BULK : NORMAL ) ); Debug(1, "Have frame type %s from score(%d) state %d frames %d bulk frame interval %d and mod%d", frame_type_names[frame_type], score, monitor->GetState(), frames, config.bulk_frame_interval, (frames % config.bulk_frame_interval)); if (score < 0) score = 0; tot_score += score; if (image) { if (save_jpegs & 1) { std::string event_file = stringtf(staticConfig.capture_file_format, path.c_str(), frames); Debug(1, "Writing capture frame %d to %s", frames, event_file.c_str()); if (!WriteFrameImage(image, timestamp, event_file.c_str())) { Error("Failed to write frame image"); } } // end if save_jpegs // If this is the first frame, we should add a thumbnail to the event directory if ((frames == 1) || (score > (int)max_score)) { write_to_db = true; // web ui might show this as thumbnail, so db needs to know about it. WriteFrameImage(image, timestamp, snapshot_file.c_str()); } // We are writing an Alarm frame if (frame_type == ALARM) { // The first frame with a score will be the frame that alarmed the event if (!alarm_frame_written) { write_to_db = true; // OD processing will need it, so the db needs to know about it alarm_frame_written = true; WriteFrameImage(image, timestamp, alarm_file.c_str()); } if (alarm_image and (save_jpegs & 2)) { std::string event_file = stringtf(staticConfig.analyse_file_format, path.c_str(), frames); Debug(1, "Writing analysis frame %d", frames); if (!WriteFrameImage(alarm_image, timestamp, event_file.c_str(), true)) { Error("Failed to write analysis frame image"); } } } // end if is an alarm frame } else { Debug(1, "No image"); } // end if has image if (frame_type == ALARM) alarm_frames++; bool db_frame = ( frame_type == BULK ) or ( frame_type == ALARM ) or ( frames == 1 ) or ( score > (int)max_score ) or ( monitor_state == Monitor::ALERT ) or ( monitor_state == Monitor::ALARM ) or ( monitor_state == Monitor::PREALARM ); if (db_frame) { struct DeltaTimeval delta_time; DELTA_TIMEVAL(delta_time, timestamp, start_time, DT_PREC_2); Debug(1, "Frame delta is %d.%d - %d.%d = %d.%d, score %u zone_stats.size %u", start_time.tv_sec, start_time.tv_usec, timestamp.tv_sec, timestamp.tv_usec, delta_time.sec, delta_time.fsec, score, zone_stats.size()); // The idea is to write out 1/sec frame_data.push(new Frame(id, frames, frame_type, timestamp, delta_time, score, zone_stats)); double fps = monitor->get_capture_fps(); if ( write_to_db or ( frame_data.size() >= MAX_DB_FRAMES ) or ( frame_type == BULK ) or ( fps and (frame_data.size() > fps) ) ) { Debug(1, "Adding %d frames to DB because write_to_db:%d or frames > analysis fps %f or BULK(%d)", frame_data.size(), write_to_db, fps, (frame_type==BULK)); WriteDbFrames(); last_db_frame = frames; char sql[ZM_SQL_MED_BUFSIZ]; snprintf(sql, sizeof(sql), "UPDATE Events SET Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64, ( delta_time.positive?"":"-" ), delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, id ); dbQueue.push(std::move(sql)); } else { Debug(1, "Not Adding %d frames to DB because write_to_db:%d or frames > analysis fps %f or BULK", frame_data.size(), write_to_db, fps); } // end if frame_type == BULK } // end if db_frame if (score > (int)max_score) max_score = score; end_time = timestamp; } // end void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *alarm_image) bool Event::SetPath(Storage *storage) { scheme = storage->Scheme(); path = stringtf("%s/%d", storage->Path(), monitor->Id()); // Try to make the Monitor Dir. Normally this would exist, but in odd cases might not. if (mkdir(path.c_str(), 0755) and (errno != EEXIST)) { Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); return false; } tm stime = {}; localtime_r(&start_time.tv_sec, &stime); if (scheme == Storage::DEEP) { int dt_parts[6]; dt_parts[0] = stime.tm_year-100; dt_parts[1] = stime.tm_mon+1; dt_parts[2] = stime.tm_mday; dt_parts[3] = stime.tm_hour; dt_parts[4] = stime.tm_min; dt_parts[5] = stime.tm_sec; std::string date_path; std::string time_path; for (unsigned int i = 0; i < sizeof(dt_parts)/sizeof(*dt_parts); i++) { path += stringtf("/%02d", dt_parts[i]); if (mkdir(path.c_str(), 0755) and (errno != EEXIST)) { Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); return false; } if (i == 2) date_path = path; } time_path = stringtf("%02d/%02d/%02d", stime.tm_hour, stime.tm_min, stime.tm_sec); // Create event id symlink std::string id_file = stringtf("%s/.%" PRIu64, date_path.c_str(), id); if (symlink(time_path.c_str(), id_file.c_str()) < 0) { Error("Can't symlink %s -> %s: %s", id_file.c_str(), time_path.c_str(), strerror(errno)); return false; } } else if (scheme == Storage::MEDIUM) { path += stringtf("/%04d-%02d-%02d", stime.tm_year+1900, stime.tm_mon+1, stime.tm_mday ); if (mkdir(path.c_str(), 0755) and (errno != EEXIST)) { Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); return false; } path += stringtf("/%" PRIu64, id); if (mkdir(path.c_str(), 0755) and (errno != EEXIST)) { Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); return false; } } else { path += stringtf("/%" PRIu64, id); if (mkdir(path.c_str(), 0755) and (errno != EEXIST)) { Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); return false; } // Create empty id tag file std::string id_file = stringtf("%s/.%" PRIu64, path.c_str(), id); if ( FILE *id_fp = fopen(id_file.c_str(), "w") ) { fclose(id_fp); } else { Error("Can't fopen %s: %s", id_file.c_str(), strerror(errno)); return false; } } // deep storage or not return true; } // end bool Event::SetPath