local cbson = require("cbson") local generate_oid = require("resty.moongoo.utils").generate_oid local _M = {} local mt = { __index = _M } function _M.new(gridfs, name, opts, safe, read_only) local read_only = read_only or false local safe = safe == nil and true or safe opts = opts or {} opts.filename = name opts.length = opts.length or cbson.uint(0) opts.chunkSize = opts.chunkSize or cbson.uint(261120) local write_only = true if not safe then write_only = false end return setmetatable( { _gridfs = gridfs, _meta = opts, _write_only = write_only, _read_only = read_only, _pos = 0, _chunks = {}, _closed = false, _buffer = '', _n = 0 }, mt) end function _M.open(gridfs, id) -- try to fetch local doc, err = gridfs._files:find_one({ _id = id}) if not doc then return nil, "No such file" else return _M.new(gridfs, doc.filename, doc, false, true) end end -- props function _M.content_type(self) return self._meta.contentType end function _M.filename(self) return self._meta.filename end function _M.md5(self) return self._meta.md5 end function _M.metadata(self) return self._meta.metadata end function _M.raw_length(self) return self._meta.length end function _M.raw_chunk_size(self) return self._meta.chunkSize end function _M.date(self) return self._meta.uploadDate end function _M.length(self) return tonumber(tostring(self._meta.length)) end function _M.chunk_size(self) return tonumber(tostring(self._meta.chunkSize)) end -- reading function _M.read(self) if self._write_only then return nil, "Can't read from write-only file" end if self._pos >= (self:length() or 0) then return nil, "EOF" end local n = math.modf(self._pos / self:chunk_size()) local query = {files_id = self._meta._id, n = n} local fields = {_id = false, data = true} local chunk = self._gridfs._chunks:find_one(query, fields) if not chunk then return nil, "EOF?" end return self:_slice(n, chunk.data) end function _M.seek(self, pos) self._pos = pos return self end function _M.tell(self) return self._pos end function _M.slurp(self) local data = {} local pos = self._pos self:seek(0) while true do local chunk = self:read() if not chunk then break end table.insert(data, chunk) end self:seek(pos) return table.concat(data) end -- writing function _M.write(self, data) if self._read_only then return nil, "Can't write to read-only file" end if self._closed then return nil, "Can't write to closed file" end self._buffer = self._buffer .. data self._meta.length = self._meta.length + data:len() while self._buffer:len() >= self:chunk_size() do local r, res = self:_chunk() if not r then return nil, err end end end function _M.close(self) if self._closed then return nil, "File already closed" end self._closed = true self:_chunk() -- enqueue/write last chunk of data if self._write_only then -- insert all collected chunks for k, v in ipairs(self._chunks) do local r, err = self._gridfs._chunks:insert(v) if not r then return nil, err end end end -- ensure indexes self._gridfs._files:ensure_index({{ key = {filename = true}}}) self._gridfs._chunks:ensure_index({ { key = {files_id = 1, n = 1}, unique = true } }); -- compute md5 local res, err = self._gridfs._db:cmd({filemd5 = self:_files_id()}, {root = self._gridfs._name}) if not res then return nil, err end local file_md5 = res.md5 -- insert metadata local ids, n = self._gridfs._files:insert(self:_metadata(file_md5)) if not ids then return nil, n end -- return metadata return ids[1] end -- private function _M._files_id(self) if not self._meta._id then self._meta._id = cbson.oid(generate_oid()) end return self._meta._id end function _M._metadata(self, file_md5) local doc = { _id = self:_files_id(), length = self:raw_length(), chunkSize = self:raw_chunk_size(), uploadDate = cbson.date(os.time(os.date('!*t'))*1000), md5 = file_md5, filename = self:filename() or nil, content_type = self:content_type() or nil, metadata = self:metadata() or nil } return doc end function _M._slice(self, n, chunk) local offset = self._pos - (n * self:chunk_size()) local chunk = chunk:raw() self._pos = self._pos + chunk:len() return chunk:sub(offset+1); end function _M._chunk(self) local chunk = self._buffer:sub(1,self:chunk_size()) if not chunk then return end self._buffer = self._buffer:sub(self:chunk_size()+1) local n = self._n self._n = self._n+1 local data = cbson.binary("") data:raw(chunk, chunk:len()) if self._write_only then -- collect chunks for insert table.insert(self._chunks, {files_id = self:_files_id(), n = cbson.uint(n), data = data}) return true else -- insert immidiately, so we can read back (ugh) return self._gridfs._chunks:insert({{files_id = self:_files_id(), n = cbson.uint(n), data = data}}) end end return _M